X-Git-Url: http://git.efficios.com/?a=blobdiff_plain;f=analysis%2Forg.eclipse.tracecompass.analysis.lami.ui%2Fsrc%2Forg%2Feclipse%2Ftracecompass%2Finternal%2Fprovisional%2Fanalysis%2Flami%2Fui%2Fviewers%2FLamiXYChartViewer.java;h=dbf5c313b8c48950b1d6c19c82e6c959877d4332;hb=9c8b380694b053cb43bbae603043f81f83c91cd1;hp=4a0a776a4a5ff836f1a05c8aedb91049bdb42618;hpb=4208b5106c351e981b78b5f93d336d39e46f4bbd;p=deliverable%2Ftracecompass.git diff --git a/analysis/org.eclipse.tracecompass.analysis.lami.ui/src/org/eclipse/tracecompass/internal/provisional/analysis/lami/ui/viewers/LamiXYChartViewer.java b/analysis/org.eclipse.tracecompass.analysis.lami.ui/src/org/eclipse/tracecompass/internal/provisional/analysis/lami/ui/viewers/LamiXYChartViewer.java index 4a0a776a4a..dbf5c313b8 100644 --- a/analysis/org.eclipse.tracecompass.analysis.lami.ui/src/org/eclipse/tracecompass/internal/provisional/analysis/lami/ui/viewers/LamiXYChartViewer.java +++ b/analysis/org.eclipse.tracecompass.analysis.lami.ui/src/org/eclipse/tracecompass/internal/provisional/analysis/lami/ui/viewers/LamiXYChartViewer.java @@ -12,6 +12,7 @@ package org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.viewers; import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString; +import java.math.BigDecimal; import java.text.Format; import java.util.ArrayList; import java.util.HashSet; @@ -23,24 +24,33 @@ import java.util.function.ToDoubleFunction; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; import org.eclipse.tracecompass.common.core.format.DecimalUnitFormat; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTableEntryAspect; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiChartModel; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiResultTable; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTableEntry; -import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTimeStampFormat; +import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiDecimalUnitFormat; +import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiTimeStampFormat; import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.signals.LamiSelectionUpdateSignal; import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler; import org.eclipse.tracecompass.tmf.ui.viewers.TmfViewer; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; import org.swtchart.Chart; import org.swtchart.ITitle; @@ -63,14 +73,10 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer */ protected static final String UNKNOWN = "?"; //$NON-NLS-1$ - /** Zero value */ - protected static final double ZERO = 0.0; - - /** Symbol for seconds (used in the custom ns -> s conversion) */ - private static final String SECONDS_SYMBOL = "s"; //$NON-NLS-1$ - - /** Symbol for nanoseconds (used in the custom ns -> s conversion) */ - private static final String NANOSECONDS_SYMBOL = "ns"; //$NON-NLS-1$ + /** Zero long value */ + protected static final long ZERO_LONG = 0L; + /** Zero double value */ + protected static final double ZERO_DOUBLE = 0.0; /** * Function to use to map Strings read from the data table to doubles for @@ -78,7 +84,7 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer */ protected static final ToDoubleFunction<@Nullable String> DOUBLE_MAPPER = str -> { if (str == null || str.equals(UNKNOWN)) { - return ZERO; + return ZERO_LONG; } return Double.parseDouble(str); }; @@ -135,12 +141,21 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer /** * Decimal formatter to display nanoseconds as seconds. */ - protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER = new DecimalUnitFormat(0.000000001); + protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER = new LamiDecimalUnitFormat(0.000000001); /** * Default decimal formatter. */ - protected static final DecimalUnitFormat DECIMAL_FORMATTER = new DecimalUnitFormat(); + protected static final DecimalUnitFormat DECIMAL_FORMATTER = new LamiDecimalUnitFormat(); + + /** Symbol for seconds (used in the custom ns -> s conversion) */ + private static final String SECONDS_SYMBOL = "s"; //$NON-NLS-1$ + + /** Symbol for nanoseconds (used in the custom ns -> s conversion) */ + private static final String NANOSECONDS_SYMBOL = "ns"; //$NON-NLS-1$ + + /** Maximum amount of digits that can be represented into a double */ + private static final int BIG_DECIMAL_DIVISION_SCALE = 22; private final Listener fResizeListener = event -> { /* Refresh the titles to fit the current chart size */ @@ -156,12 +171,18 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer private final Chart fChart; private final String fChartTitle; - private final String fXTitle; - private final String fYTitle; + + private String fXLabel; + private @Nullable String fXUnits; + + private String fYLabel; + private @Nullable String fYUnits; private boolean fSelected; private Set fSelection; + private final ToolBar fToolBar; + /** * Creates a Viewer instance based on SWTChart. * @@ -181,6 +202,9 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer fChartModel = chartModel; fSelection = new HashSet<>(); + fXLabel = ""; //$NON-NLS-1$ + fYLabel = ""; //$NON-NLS-1$ + fChart = new Chart(parent, SWT.NONE); fChart.addListener(SWT.Resize, fResizeListener); @@ -191,37 +215,36 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer if (fChartModel.getXSeriesColumns().size() == 1) { /* * There is only 1 series in the chart, we will use its name as the - * Y axis (and hide the legend). + * X axis. */ - String seriesName = getChartModel().getXSeriesColumns().get(0); - // The time duration formatter converts ns to s on the axis - if (NANOSECONDS_SYMBOL.equals(getXAxisAspects().get(0).getUnits())) { - seriesName = getXAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL + ')'; //$NON-NLS-1$ - } - fXTitle = seriesName; + innerSetXTitle(getXAxisAspects().get(0).getName(), getXAxisAspects().get(0).getUnits()); } else { /* * There are multiple series in the chart, if they all share the same * units, display that. */ - long nbDiffAspects = getXAxisAspects().stream() + long nbDiffAspectsUnits = getXAxisAspects().stream() .map(aspect -> aspect.getUnits()) .distinct() .count(); - String units = getXAxisAspects().get(0).getUnits(); - if (nbDiffAspects == 1 && units != null) { - /* All aspects use the same unit type */ + long nbDiffAspectName = getXAxisAspects().stream() + .map(aspect -> aspect.getName()) + .distinct() + .count(); - // The time duration formatter converts ns to s on the axis - if (NANOSECONDS_SYMBOL.equals(units)) { - units = SECONDS_SYMBOL; - } - fXTitle = Messages.LamiViewer_DefaultValueName + " (" + units + ')'; //$NON-NLS-1$ - } else { - /* Various unit types, just say "Value" */ - fXTitle = nullToEmptyString(Messages.LamiViewer_DefaultValueName); + String xBaseTitle = Messages.LamiViewer_DefaultValueName; + if (nbDiffAspectName == 1) { + xBaseTitle = getXAxisAspects().get(0).getName(); } + + String units = null; + if (nbDiffAspectsUnits == 1) { + /* All aspects use the same unit type */ + units = getXAxisAspects().get(0).getUnits(); + } + + innerSetXTitle(xBaseTitle, units); } /* Set Y axis title */ @@ -230,37 +253,38 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer * There is only 1 series in the chart, we will use its name as the * Y axis (and hide the legend). */ - String seriesName = getChartModel().getYSeriesColumns().get(0); - // The time duration formatter converts ns to s on the axis - if (NANOSECONDS_SYMBOL.equals(getYAxisAspects().get(0).getUnits())) { - seriesName = getYAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL + ')'; //$NON-NLS-1$ - } - fYTitle = seriesName; + innerSetYTitle(getYAxisAspects().get(0).getName(), getYAxisAspects().get(0).getUnits()); + + /* Hide the legend */ fChart.getLegend().setVisible(false); } else { /* * There are multiple series in the chart, if they all share the same * units, display that. */ - long nbDiffAspects = getYAxisAspects().stream() + long nbDiffAspectsUnits = getYAxisAspects().stream() .map(aspect -> aspect.getUnits()) .distinct() .count(); - String units = getYAxisAspects().get(0).getUnits(); - if (nbDiffAspects == 1 && units != null) { - /* All aspects use the same unit type */ + long nbDiffAspectName = getYAxisAspects().stream() + .map(aspect -> aspect.getName()) + .distinct() + .count(); - // The time duration formatter converts ns to s on the axis - if (NANOSECONDS_SYMBOL.equals(units)) { - units = SECONDS_SYMBOL; - } - fYTitle = Messages.LamiViewer_DefaultValueName + " (" + units + ')'; //$NON-NLS-1$ - } else { - /* Various unit types, just say "Value" */ - fYTitle = nullToEmptyString(Messages.LamiViewer_DefaultValueName); + String yBaseTitle = Messages.LamiViewer_DefaultValueName; + if (nbDiffAspectName == 1) { + yBaseTitle = getYAxisAspects().get(0).getName(); } + String units = null; + if (nbDiffAspectsUnits == 1) { + /* All aspects use the same unit type */ + units = getYAxisAspects().get(0).getUnits(); + } + + innerSetYTitle(yBaseTitle, units); + /* Put legend at the bottom */ fChart.getLegend().setPosition(SWT.BOTTOM); } @@ -278,12 +302,130 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer /* Refresh the titles to fit the current chart size */ refreshDisplayTitles(); + fToolBar = createChartToolBar(); + fChart.addDisposeListener(e -> { /* Dispose resources of this class */ LamiXYChartViewer.super.dispose(); }); } + /** + * Set the Y axis title and refresh the chart. + * + * @param label + * the label string. + * @param units + * the units string. + */ + protected void setYTitle(@Nullable String label, @Nullable String units) { + innerSetYTitle(label, units); + } + + private void innerSetYTitle(@Nullable String label, @Nullable String units) { + fYLabel = nullToEmptyString(label); + innerSetYUnits(units); + refreshDisplayTitles(); + } + + /** + * Set the units on the Y Axis title and refresh the chart. + * + * @param units + * the units string. + */ + protected void setYUnits(@Nullable String units) { + innerSetYUnits(units); + } + + private void innerSetYUnits(@Nullable String units) { + /* + * All time durations in the Lami protocol are nanoseconds, on the + * charts we use an axis formater that converts back to seconds as a + * base unit and then uses prefixes like nano and milli depending on the + * range. + * + * So set the units to seconds in the title to match the base unit of + * the formater. + */ + if (NANOSECONDS_SYMBOL.equals(units)) { + fYUnits = SECONDS_SYMBOL; + } else { + fYUnits = units; + } + refreshDisplayTitles(); + } + + /** + * Get the Y axis title string. + * + * If the units is non-null, the title will be: "label (units)" + * + * If the units is null, the title will be: "label" + * + * @return the title of the Y axis. + */ + protected String getYTitle() { + if (fYUnits == null) { + return fYLabel; + } + return fYLabel + " (" + fYUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + } + + /** + * Set the X axis title and refresh the chart. + * + * @param label + * the label string. + * @param units + * the units string. + */ + protected void setXTitle(@Nullable String label, @Nullable String units) { + innerSetXTitle(label, units); + } + + private void innerSetXTitle(@Nullable String label, @Nullable String units) { + fXLabel = nullToEmptyString(label); + innerSetXUnits(units); + refreshDisplayTitles(); + } + + /** + * Set the units on the X Axis title. + * + * @param units + * the units string + */ + protected void setXUnits(@Nullable String units) { + innerSetXUnits(units); + } + + private void innerSetXUnits(@Nullable String units) { + /* The time duration formatter converts ns to s on the axis */ + if (NANOSECONDS_SYMBOL.equals(units)) { + fXUnits = SECONDS_SYMBOL; + } else { + fXUnits = units; + } + refreshDisplayTitles(); + } + + /** + * Get the X axis title string. + * + * If the units is non-null, the title will be: "label (units)" + * + * If the units is null, the title will be: "label" + * + * @return the title of the Y axis. + */ + protected String getXTitle() { + if (fXUnits == null) { + return fXLabel; + } + return fXLabel + " (" + fXUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$ + } + /** * Util method to check if a list of aspects are all continuous. * @@ -328,45 +470,64 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer * The list of aspects of the axis. * @param entries * The list of entries of the chart. + * @param internalRange + * The internal range for value transformation + * @param externalRange + * The external range for value transformation * @return a formatter for the axis. */ - protected static Format getContinuousAxisFormatter(List axisAspects, List entries) { + protected static Format getContinuousAxisFormatter(List axisAspects, List entries , @Nullable LamiGraphRange internalRange, @Nullable LamiGraphRange externalRange) { + + Format formatter = DECIMAL_FORMATTER; if (areAspectsTimeStamp(axisAspects)) { /* Set a TimeStamp formatter depending on the duration between the first and last value */ - double max = Double.MIN_VALUE; - double min = Double.MAX_VALUE; + BigDecimal max = new BigDecimal(Long.MIN_VALUE); + BigDecimal min = new BigDecimal(Long.MAX_VALUE); for (LamiTableEntry entry : entries) { for (LamiTableEntryAspect aspect : axisAspects) { - Double current = aspect.resolveDouble(entry); - if (current != null) { - max = Math.max(max, current); - min = Math.min(min, current); + @Nullable Number number = aspect.resolveNumber(entry); + if (number != null) { + BigDecimal current = new BigDecimal(number.toString()); + max = current.max(max); + min = current.min(min); } } } - long duration = (long) max - (long) min; + long duration = max.subtract(min).longValue(); if (duration > TimeUnit.DAYS.toNanos(1)) { - return DAYS_FORMATTER; + formatter = DAYS_FORMATTER; } else if (duration > TimeUnit.HOURS.toNanos(1)) { - return HOURS_FORMATTER; + formatter = HOURS_FORMATTER; } else if (duration > TimeUnit.MINUTES.toNanos(1)) { - return MINUTES_FORMATTER; + formatter = MINUTES_FORMATTER; } else if (duration > TimeUnit.SECONDS.toNanos(15)) { - return SECONDS_FORMATTER; + formatter = SECONDS_FORMATTER; } else { - return MILLISECONDS_FORMATTER; + formatter = MILLISECONDS_FORMATTER; } + ((LamiTimeStampFormat) formatter).setInternalRange(internalRange); + ((LamiTimeStampFormat) formatter).setExternalRange(externalRange); + } else if (areAspectsTimeDuration(axisAspects)) { - /* Set the time duration formatter */ - return NANO_TO_SECS_FORMATTER; + /* Set the time duration formatter. */ + formatter = NANO_TO_SECS_FORMATTER; + ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange); + ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange); } else { - /* For other numeric aspects, use the default decimal unit formatter */ - return DECIMAL_FORMATTER; + /* + * For other numeric aspects, use the default lami decimal unit + * formatter. + */ + formatter = DECIMAL_FORMATTER; + ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange); + ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange); } + + return formatter; } /** @@ -395,6 +556,13 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer return fChart; } + /** + * @return the toolBar + */ + public ToolBar getToolBar() { + return fToolBar; + } + /** * Is a selection made in the chart. * @@ -538,10 +706,10 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer refreshDisplayTitle(chartTitle, fChartTitle, chartRect.width); ITitle xTitle = checkNotNull(fChart.getAxisSet().getXAxis(0).getTitle()); - refreshDisplayTitle(xTitle, fXTitle, plotRect.width); + refreshDisplayTitle(xTitle, getXTitle(), plotRect.width); ITitle yTitle = checkNotNull(fChart.getAxisSet().getYAxis(0).getTitle()); - refreshDisplayTitle(yTitle, fYTitle, plotRect.height); + refreshDisplayTitle(yTitle, getYTitle(), plotRect.height); } /** @@ -592,4 +760,165 @@ public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer redraw(); } + + /** + * Create a tool bar on top right of the chart. Contained actions: + *
    + *
  • Dispose the current viewer, also known as "Close the chart"
  • + *
+ * + * This tool bar should only appear when the mouse enters the composite. + * + * @return the tool bar + */ + protected ToolBar createChartToolBar() { + Image removeImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ELCL_REMOVE); + ToolBar toolBar = new ToolBar(getChart(), SWT.HORIZONTAL); + + /* Default state */ + toolBar.moveAbove(null); + toolBar.setVisible(false); + + /* + * Close chart button + */ + ToolItem closeButton = new ToolItem(toolBar, SWT.PUSH); + closeButton.setImage(removeImage); + closeButton.setToolTipText(Messages.LamiXYChartViewer_CloseChartToolTip); + closeButton.addSelectionListener(new SelectionListener() { + @Override + public void widgetSelected(@Nullable SelectionEvent e) { + Composite parent = getParent(); + dispose(); + parent.layout(); + } + + @Override + public void widgetDefaultSelected(@Nullable SelectionEvent e) { + } + }); + + toolBar.pack(); + toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); + + /* Visibility toggle filter */ + Listener toolBarVisibilityToggleListener = e -> { + if (e.widget instanceof Control) { + Control control = (Control) e.widget; + Point display = control.toDisplay(e.x, e.y); + Point location = getChart().getParent().toControl(display); + + /* + * Only set to visible if we are at the right location, in the + * right shell. + */ + boolean visible = getChart().getBounds().contains(location) && + control.getShell().equals(getChart().getShell()); + getToolBar().setVisible(visible); + } + }; + + /* Filter to make sure we hide the toolbar if we exit the window */ + Listener hideToolBarListener = (e -> getToolBar().setVisible(false)); + + /* + * Add the filters to the main Display, and remove them when we dispose + * the chart. + */ + Display display = getChart().getDisplay(); + display.addFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); + display.addFilter(SWT.MouseExit, hideToolBarListener); + + getChart().addDisposeListener(e -> { + display.removeFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); + display.removeFilter(SWT.MouseExit, hideToolBarListener); + }); + + /* Reposition the tool bar on resize */ + getChart().addListener(SWT.Resize, new Listener() { + @Override + public void handleEvent(@Nullable Event event) { + toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); + } + }); + + return toolBar; + } + + /** + * Get a {@link LamiGraphRange} that covers all data points in the result + * table. + *

+ * The returned range will be the minimum and maximum of the resolved values + * of the passed aspects for all result entries. If clampToZero + * is true, a positive minimum value will be clamped down to zero. + * + * @param aspects + * The aspects that the range will represent + * @param clampToZero + * If true, a positive minimum value will be clamped down to zero + * @return the range + */ + protected LamiGraphRange getRange(List aspects, boolean clampToZero) { + /* Find the minimum and maximum values */ + BigDecimal min = new BigDecimal(Long.MAX_VALUE); + BigDecimal max = new BigDecimal(Long.MIN_VALUE); + for (LamiTableEntryAspect lamiTableEntryAspect : aspects) { + for (LamiTableEntry entry : getResultTable().getEntries()) { + @Nullable Number number = lamiTableEntryAspect.resolveNumber(entry); + if (number != null) { + BigDecimal current = new BigDecimal(number.toString()); + min = current.min(min); + max = current.max(max); + } + } + } + + if (clampToZero) { + min = min.min(BigDecimal.ZERO); + } + + /* Do not allow a range with a zero delta default to 1 */ + if (max.equals(min)) { + max = min.add(BigDecimal.ONE); + } + + return new LamiGraphRange(checkNotNull(min), checkNotNull(max)); + } + + /** + * Transform an external value into an internal value. Since SWTChart only + * support Double and Lami can pass Long values, loss of precision might + * happen. To minimize this, transform the raw values to an internal + * representation based on a linear transformation. + * + * The internal value = + * + * ((rawValue - rawMinimum) * (internalRangeDelta/rawRangeDelta)) + + * internalMinimum + * + * @param number + * The number to transform + * @param internalRange + * The internal range definition to be used + * @param externalRange + * The external range definition to be used + * @return the transformed value in Double comprised inside the internal + * range + */ + protected static double getInternalDoubleValue(Number number, LamiGraphRange internalRange, LamiGraphRange externalRange) { + BigDecimal value = new BigDecimal(number.toString()); + + if (externalRange.getDelta().compareTo(BigDecimal.ZERO) == 0) { + return internalRange.getMinimum().doubleValue(); + } + + BigDecimal internalValue = value + .subtract(externalRange.getMinimum()) + .multiply(internalRange.getDelta()) + .divide(externalRange.getDelta(), BIG_DECIMAL_DIVISION_SCALE, BigDecimal.ROUND_DOWN) + .add(internalRange.getMinimum()); + + return internalValue.doubleValue(); + } }