/******************************************************************************* * Copyright (c) 2014, 2015 École Polytechnique de Montréal and others. * * All rights reserved. This program and the accompanying materials are * made available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Geneviève Bastien - Initial API and implementation *******************************************************************************/ package org.eclipse.tracecompass.tmf.ui.viewers.xycharts.linecharts; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.tracecompass.common.core.log.TraceCompassLog; import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager; import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace; import org.eclipse.tracecompass.tmf.ui.signal.TmfTimeViewAlignmentInfo; import org.eclipse.tracecompass.tmf.ui.signal.TmfTimeViewAlignmentSignal; import org.eclipse.tracecompass.tmf.ui.viewers.xycharts.TmfChartTimeStampFormat; import org.eclipse.tracecompass.tmf.ui.viewers.xycharts.TmfXYChartViewer; import org.swtchart.IAxisTick; import org.swtchart.ILineSeries; import org.swtchart.ILineSeries.PlotSymbolType; import org.swtchart.ISeries; import org.swtchart.ISeries.SeriesType; import org.swtchart.ISeriesSet; import org.swtchart.LineStyle; import org.swtchart.Range; /** * Abstract line chart viewer class implementation. All series in this viewer * use the same X axis values. They are automatically created as values are * provided for a key. Series by default will be displayed as a line. Each * series appearance can be overridden when creating it. * * @author - Geneviève Bastien */ public abstract class TmfCommonXLineChartViewer extends TmfXYChartViewer { private static final double DEFAULT_MAXY = Double.MIN_VALUE; private static final double DEFAULT_MINY = Double.MAX_VALUE; /* The desired number of points per pixel */ private static final double RESOLUTION = 1.0; private static final Logger LOGGER = TraceCompassLog.getLogger(TmfCommonXLineChartViewer.class); private static final String LOG_STRING_WITH_PARAM = "[TmfCommonXLineChart:%s] viewerId=%s, %s"; //$NON-NLS-1$ private static final String LOG_STRING = "[TmfCommonXLineChart:%s] viewerId=%s"; //$NON-NLS-1$ private static final int[] LINE_COLORS = { SWT.COLOR_BLUE, SWT.COLOR_RED, SWT.COLOR_GREEN, SWT.COLOR_MAGENTA, SWT.COLOR_CYAN, SWT.COLOR_DARK_BLUE, SWT.COLOR_DARK_RED, SWT.COLOR_DARK_GREEN, SWT.COLOR_DARK_MAGENTA, SWT.COLOR_DARK_CYAN, SWT.COLOR_DARK_YELLOW, SWT.COLOR_BLACK, SWT.COLOR_GRAY }; private static final LineStyle[] LINE_STYLES = { LineStyle.SOLID, LineStyle.DASH, LineStyle.DOT, LineStyle.DASHDOT }; private final Map<String, double[]> fSeriesValues = new LinkedHashMap<>(); private double[] fXValues; private double fResolution; private UpdateThread fUpdateThread; private final AtomicInteger fDirty = new AtomicInteger(); /** * Constructor * * @param parent * The parent composite * @param title * The title of the viewer * @param xLabel * The label of the xAxis * @param yLabel * The label of the yAXIS */ public TmfCommonXLineChartViewer(Composite parent, String title, String xLabel, String yLabel) { super(parent, title, xLabel, yLabel); getSwtChart().getTitle().setVisible(false); getSwtChart().getLegend().setPosition(SWT.BOTTOM); getSwtChart().getAxisSet().getXAxes()[0].getTitle().setVisible(false); setResolution(RESOLUTION); setTooltipProvider(new TmfCommonXLineChartTooltipProvider(this)); } /** * Set the number of requests per pixel that should be done on this chart * * @param resolution * The number of points per pixels */ protected void setResolution(double resolution) { fResolution = resolution; } @Override public void loadTrace(ITmfTrace trace) { super.loadTrace(trace); reinitialize(); } /** * Formats a log message for this class * * @param event * The event to log, that will be appended to the class name to * make the full event name * @param parameters * The string of extra parameters to add to the log message, in * the format name=value[, name=value]*, or <code>null</code> for * no params * @return The complete log message for this class */ private String getLogMessage(String event, @Nullable String parameters) { if (parameters == null) { return String.format(LOG_STRING, event, getClass().getName()); } return String.format(LOG_STRING_WITH_PARAM, event, getClass().getName(), parameters); } /** * Forces a reinitialization of the data sources, even if it has already * been initialized for this trace before */ protected void reinitialize() { fSeriesValues.clear(); /* Initializing data: the content is not current */ fDirty.incrementAndGet(); Thread thread = new Thread() { // Don't use TmfUiRefreshHandler (bug 467751) @Override public void run() { LOGGER.info(() -> getLogMessage("InitializeThreadStart", "tid=" + getId())); //$NON-NLS-1$ //$NON-NLS-2$ initializeDataSource(); if (!getSwtChart().isDisposed()) { getDisplay().asyncExec(new Runnable() { @Override public void run() { if (!getSwtChart().isDisposed()) { /* Delete the old series */ try { clearContent(); createSeries(); } finally { /* View is cleared, decrement fDirty */ fDirty.decrementAndGet(); } } } }); } LOGGER.info(() -> getLogMessage("InitializeThreadEnd", "tid=" + getId())); //$NON-NLS-1$ //$NON-NLS-2$ } }; thread.start(); } /** * Initialize the source of the data for this viewer. This method is run in * a separate thread, so this is where for example one can execute an * analysis module and wait for its completion to initialize the series */ protected void initializeDataSource() { } private class UpdateThread extends Thread { private final IProgressMonitor fMonitor; private final int fNumRequests; public UpdateThread(int numRequests) { super("Line chart update"); //$NON-NLS-1$ fNumRequests = numRequests; fMonitor = new NullProgressMonitor(); } @Override public void run() { LOGGER.info(() -> getLogMessage("UpdateThreadStart", "numRequests=" + fNumRequests + ", tid=" + getId())); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ try { updateData(getWindowStartTime(), getWindowEndTime(), fNumRequests, fMonitor); } finally { /* * fDirty should have been incremented before creating the * thread, so we decrement it once it is finished */ fDirty.decrementAndGet(); } updateThreadFinished(this); LOGGER.info(() -> getLogMessage("UpdateThreadEnd", "tid=" + getId())); //$NON-NLS-1$ //$NON-NLS-2$ } public void cancel() { LOGGER.info(() -> getLogMessage("UpdateThreadCanceled", "tid=" + getId())); //$NON-NLS-1$ //$NON-NLS-2$ fMonitor.setCanceled(true); } } private synchronized void newUpdateThread() { cancelUpdate(); if (!getSwtChart().isDisposed()) { final int numRequests = (int) (getSwtChart().getPlotArea().getBounds().width * fResolution); fUpdateThread = new UpdateThread(numRequests); fUpdateThread.start(); } } private synchronized void updateThreadFinished(UpdateThread thread) { if (thread == fUpdateThread) { fUpdateThread = null; } } /** * Cancels the currently running update thread. It is automatically called * when the content is updated, but child viewers may want to call it * manually to do some operations before calling * {@link TmfCommonXLineChartViewer#updateContent} */ protected synchronized void cancelUpdate() { if (fUpdateThread != null) { fUpdateThread.cancel(); } } @Override protected void updateContent() { /* * Content is not up to date, so we increment fDirty. It will be * decremented at the end of the update thread */ fDirty.incrementAndGet(); getDisplay().asyncExec(new Runnable() { @Override public void run() { newUpdateThread(); } }); } /** * Convenience method to compute the values of the X axis for a given time * range. This method will return at most nb values, equally separated from * start to end. The step between values will be at least 1.0, so the number * of values returned can be lower than nb. * * The returned time values are in internal time, ie to get trace time, the * time offset needs to be added to those values. * * @param start * The start time of the time range * @param end * End time of the range * @param nb * The maximum number of steps in the x axis. * @return The time values (converted to double) to match every step. */ protected static final double[] getXAxis(long start, long end, int nb) { long steps = (end - start); int nbVals = nb; if (steps < nb) { nbVals = (int) steps; if (nbVals <= 0) { nbVals = 1; } } double step = steps / (double) nbVals; double timestamps[] = new double[nbVals]; double curTime = 1; for (int i = 0; i < nbVals; i++) { timestamps[i] = curTime; curTime += step; } return timestamps; } /** * Set the values of the x axis. There is only one array of values for the x * axis for all series of a line chart so it needs to be set once here. * * @param xaxis * The values for the x axis. The values must be in internal * time, ie time offset have been subtracted from trace time * values. */ protected final void setXAxis(double[] xaxis) { fXValues = xaxis; } /** * Update the series data because the time range has changed. The x axis * values for this data update can be computed using the * {@link TmfCommonXLineChartViewer#getXAxis(long, long, int)} method which * will return a list of uniformely separated time values. * * Each series values should be set by calling the * {@link TmfCommonXLineChartViewer#setSeries(String, double[])}. * * This method is responsible for calling the * {@link TmfCommonXLineChartViewer#updateDisplay()} when needed for the new * values to be displayed. * * @param start * The start time of the range for which the get the data * @param end * The end time of the range * @param nb * The number of 'points' in the chart. * @param monitor * The progress monitor object */ protected abstract void updateData(long start, long end, int nb, IProgressMonitor monitor); /** * Set the data for a given series of the graph. The series does not need to * be created before calling this, but it needs to have at least as many * values as the x axis. * * If the series does not exist, it will automatically be created at display * time, with the default values. * * @param seriesName * The name of the series for which to set the values * @param seriesValues * The array of values for the series */ protected void setSeries(String seriesName, double[] seriesValues) { if (fXValues.length != seriesValues.length) { throw new IllegalStateException("All series in list must be of length : " + fXValues.length); //$NON-NLS-1$ } fSeriesValues.put(seriesName, seriesValues); } /** * Add a new series to the XY line chart. By default, it is a simple solid * line. * * @param seriesName * The name of the series to create * @return The series so that the concrete viewer can modify its properties * if required */ protected ILineSeries addSeries(String seriesName) { ISeriesSet seriesSet = getSwtChart().getSeriesSet(); int seriesCount = seriesSet.getSeries().length; ILineSeries series = (ILineSeries) seriesSet.createSeries(SeriesType.LINE, seriesName); series.setVisible(true); series.enableArea(false); series.setLineStyle(LINE_STYLES[(seriesCount / (LINE_COLORS.length)) % LINE_STYLES.length]); series.setSymbolType(PlotSymbolType.NONE); series.setLineColor(Display.getDefault().getSystemColor(LINE_COLORS[seriesCount % LINE_COLORS.length])); return series; } /** * Delete a series from the chart and its values from the viewer. * * @param seriesName * Name of the series to delete */ protected void deleteSeries(String seriesName) { ISeries series = getSwtChart().getSeriesSet().getSeries(seriesName); if (series != null) { getSwtChart().getSeriesSet().deleteSeries(series.getId()); } fSeriesValues.remove(seriesName); } /** * Update the chart's values before refreshing the viewer */ protected void updateDisplay() { /* Content is not up to date, increment dirtiness */ fDirty.incrementAndGet(); Display.getDefault().asyncExec(new Runnable() { final TmfChartTimeStampFormat tmfChartTimeStampFormat = new TmfChartTimeStampFormat(getTimeOffset()); @Override public void run() { try { LOGGER.info(() -> getLogMessage("UpdateDisplayStart", null)); //$NON-NLS-1$ if (!getSwtChart().isDisposed()) { double[] xValues = fXValues; double maxy = DEFAULT_MAXY; double miny = DEFAULT_MINY; for (Entry<String, double[]> entry : fSeriesValues.entrySet()) { ILineSeries series = (ILineSeries) getSwtChart().getSeriesSet().getSeries(entry.getKey()); if (series == null) { series = addSeries(entry.getKey()); } series.setXSeries(xValues); /* * Find the minimal and maximum values in this * series */ for (double value : entry.getValue()) { maxy = Math.max(maxy, value); miny = Math.min(miny, value); } series.setYSeries(entry.getValue()); } if (maxy == DEFAULT_MAXY) { maxy = 1.0; } IAxisTick xTick = getSwtChart().getAxisSet().getXAxis(0).getTick(); xTick.setFormat(tmfChartTimeStampFormat); final double start = 0.0; double end = getWindowEndTime() - getWindowStartTime(); getSwtChart().getAxisSet().getXAxis(0).setRange(new Range(start, end)); if (maxy > miny) { getSwtChart().getAxisSet().getYAxis(0).setRange(new Range(miny, maxy)); } getSwtChart().redraw(); if (isSendTimeAlignSignals()) { // The width of the chart might have changed and its // time axis might be misaligned with the other // views Point viewPos = TmfCommonXLineChartViewer.this.getParent().getParent().toDisplay(0, 0); int axisPos = getSwtChart().toDisplay(0, 0).x + getPointAreaOffset(); int timeAxisOffset = axisPos - viewPos.x; TmfTimeViewAlignmentInfo timeAlignmentInfo = new TmfTimeViewAlignmentInfo(getControl().getShell(), viewPos, timeAxisOffset); TmfSignalManager.dispatchSignal(new TmfTimeViewAlignmentSignal(TmfCommonXLineChartViewer.this, timeAlignmentInfo, true)); } } LOGGER.info(() -> getLogMessage("UpdateDisplayEnd", null)); //$NON-NLS-1$ } finally { /* Content has been updated, decrement dirtiness */ fDirty.decrementAndGet(); } } }); } /** * Create the series once the initialization of the viewer's data source is * done. Series do not need to be created before setting their values, but * if their appearance needs to be customized, this method is a good place * to do so. It is called only once per trace. */ protected void createSeries() { } @Override protected void clearContent() { getSwtChart().getAxisSet().getXAxis(0).getTick().setFormat(null); super.clearContent(); } @Override public boolean isDirty() { /* Check the parent's or this view's own dirtiness */ return super.isDirty() || (fDirty.get() != 0); } }