/*******************************************************************************
* Copyright (c) 2016 EfficiOS Inc., Alexandre Montplaisir
*
* 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
*******************************************************************************/
package org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.views;
import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiEmptyAspect;
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.LamiChartModel.ChartType;
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.LamiXYSeriesDescription;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiTimeRange;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.signals.LamiSelectionUpdateSignal;
import org.eclipse.tracecompass.tmf.core.component.TmfComponent;
import org.eclipse.tracecompass.tmf.core.signal.TmfSelectionRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
import com.google.common.collect.Iterables;
/**
* Sub-view of a {@link LamiReportView} that shows the contents of one table of
* the analysis report. While it is not a View object directly, its
* responsibilities are the same.
*
* @author Alexandre Montplaisir
* @author Jonathan Rajotte-Julien
*/
public final class LamiReportViewTabPage extends TmfComponent {
// ------------------------------------------------------------------------
// Attributes
// ------------------------------------------------------------------------
private final LamiResultTable fResultTable;
private final LamiViewerControl fTableViewerControl;
private final Set<LamiViewerControl> fCustomGraphViewerControls = new LinkedHashSet<>();
private final Composite fControl;
private Set<Integer> fSelectionIndexes;
// ------------------------------------------------------------------------
// Constructor
// ------------------------------------------------------------------------
/**
* Constructor
*
* @param parent
* Parent composite
* @param table
* The result table to display in this tab
*/
public LamiReportViewTabPage(Composite parent, LamiResultTable table) {
super(table.getTableClass().getTableTitle());
fResultTable = table;
fSelectionIndexes = new HashSet<>();
fSelectionIndexes = getIndexOfEntriesIntersectingTimerange(checkNotNull(fResultTable), TmfTraceManager.getInstance().getCurrentTraceContext().getSelectionRange());
fControl = parent;
/* Prepare the table viewer, which is always present */
LamiViewerControl tableViewerControl = new LamiViewerControl(fControl, this);
fTableViewerControl = tableViewerControl;
/* Automatically open the table viewer initially */
tableViewerControl.getToggleAction().run();
/* Simulate a new external signal to the default viewer */
LamiSelectionUpdateSignal signal = new LamiSelectionUpdateSignal(LamiReportViewTabPage.this, fSelectionIndexes, this);
TmfSignalManager.dispatchSignal(signal);
fControl.addDisposeListener(e -> {
/* Dispose this class's resource */
fTableViewerControl.dispose();
clearAllCustomViewers();
super.dispose();
});
}
// ------------------------------------------------------------------------
// Operations
// ------------------------------------------------------------------------
@Override
public void dispose() {
fControl.dispose();
/* fControl's disposeListener will dispose the class's resources */
}
/**
* Get the SWT control associated with this tab page.
*
* @return The SWT control
*/
public Composite getControl() {
return fControl;
}
/**
* Get the result table shown in this tab.
*
* @return The report result table
*/
public LamiResultTable getResultTable() {
return fResultTable;
}
/**
* Clear all the custom graph viewers in this tab.
*/
public void clearAllCustomViewers() {
fCustomGraphViewerControls.forEach(LamiViewerControl::dispose);
fCustomGraphViewerControls.clear();
}
/**
* Toggle the display of the table viewer in this tab. This shows it if it
* is currently hidden, and vice versa.
*/
public void toggleTableViewer() {
fTableViewerControl.getToggleAction().run();
}
/**
* Add a new chart viewer to this tab.
*
* The method only needs a chart type (currently selected via separate
* actions), all other information will be found in the result table or in
* dialogs shown to the user as part of the execution of this method.
*
* @param chartType
* The type of chart to create
*/
public void createNewCustomChart(ChartType chartType) {
int xLogScaleOptionIndex = -1;
int yLogScaleOptionIndex = -1;
List<LamiTableEntryAspect> xStringColumn = fResultTable.getTableClass().getAspects().stream()
.filter(aspect -> !(aspect instanceof LamiEmptyAspect))
.collect(Collectors.toList());
/* Get the flattened aspects for Y since mapping an aggregate aspect to y series make no sense so far */
List<LamiTableEntryAspect> yStringColumn = fResultTable.getTableClass().getAspects().stream()
.filter(aspect -> !(aspect instanceof LamiEmptyAspect))
.collect(Collectors.toList());
switch (chartType) {
case BAR_CHART:
/* Y value must strictly continous and non timestamp */
yStringColumn = yStringColumn.stream()
.filter(aspect -> !aspect.isTimeStamp() && aspect.isContinuous())
.collect(Collectors.toList());
break;
case PIE_CHART:
break;
case XY_SCATTER:
break;
default:
break;
}
IStructuredContentProvider contentProvider = checkNotNull(ArrayContentProvider.getInstance());
LamiSeriesDialog dialog = new LamiSeriesDialog(getControl().getShell(),
chartType,
xStringColumn,
yStringColumn,
contentProvider,
new LabelProvider() {
@Override
public String getText(@Nullable Object element) {
return ((LamiTableEntryAspect) checkNotNull(element)).getLabel();
}
},
contentProvider,
new LabelProvider() {
@Override
public String getText(@Nullable Object element) {
return ((LamiTableEntryAspect) checkNotNull(element)).getLabel();
}
});
dialog.setTitle(chartType.toString() + ' ' + Messages.LamiSeriesDialog_creation);
/* X options per chart type */
switch (chartType) {
case XY_SCATTER:
xLogScaleOptionIndex = dialog.addXCheckBoxOption(
Messages.LamiSeriesDialog_x_axis + ' ' + Messages.LamiReportView_LogScale,
false, new Predicate<LamiTableEntryAspect>() {
@Override
public boolean test(@NonNull LamiTableEntryAspect t) {
return t.isContinuous() && !t.isTimeStamp();
}
});
break;
case BAR_CHART:
case PIE_CHART:
default:
break;
}
/* Y options per chart type */
switch (chartType) {
case BAR_CHART:
case XY_SCATTER:
yLogScaleOptionIndex = dialog.addYCheckBoxOption(
Messages.LamiSeriesDialog_y_axis + ' ' + Messages.LamiReportView_LogScale,
false, new Predicate<LamiTableEntryAspect>() {
@Override
public boolean test(@NonNull LamiTableEntryAspect t) {
return t.isContinuous() && !t.isTimeStamp();
}
});
break;
case PIE_CHART:
default:
break;
}
if (dialog.open() != Window.OK) {
return;
}
List<LamiXYSeriesDescription> results = Arrays.stream(dialog.getResult())
.map(serie -> (LamiXYSeriesDescription) serie)
.collect(Collectors.toList());
boolean[] xCheckBoxOptionsResults = dialog.getXCheckBoxOptionValues();
boolean[] yCheckBoxOptionsResults = dialog.getYCheckBoxOptionValues();
boolean isXLogScale = false;
boolean isYLogScale = false;
/* Get X log scale option */
if (xLogScaleOptionIndex > -1 && xLogScaleOptionIndex < xCheckBoxOptionsResults.length) {
isXLogScale = xCheckBoxOptionsResults[xLogScaleOptionIndex];
}
/* Get Y log scale option */
if (yLogScaleOptionIndex > -1 && yLogScaleOptionIndex < yCheckBoxOptionsResults.length) {
isYLogScale = yCheckBoxOptionsResults[yLogScaleOptionIndex];
}
List<String> xAxisColString = new ArrayList<>();
List<String> yAxisColString = new ArrayList<>();
/* Specific chart type result fetching */
switch (chartType) {
case PIE_CHART:
case BAR_CHART:
/* Validate that we only have 1 X aspect */
if (results.stream()
.map(element -> element.getXAspect().getLabel())
.distinct()
.count() != 1) {
throw new IllegalStateException();
}
xAxisColString = results.stream()
.map(element -> element.getXAspect().getLabel())
.distinct()
.collect(Collectors.toList());
break;
case XY_SCATTER:
xAxisColString = results.stream()
.map(element -> element.getXAspect().getLabel())
.collect(Collectors.toList());
break;
default:
break;
}
yAxisColString = results.stream()
.map(element -> element.getYAspect().getLabel())
.collect(Collectors.toList());
LamiChartModel model = new LamiChartModel(chartType,
nullToEmptyString(Messages.LamiReportView_Custom),
xAxisColString,
yAxisColString,
isXLogScale,
isYLogScale);
LamiViewerControl viewerControl = new LamiViewerControl(fControl, this, model);
fCustomGraphViewerControls.add(viewerControl);
viewerControl.getToggleAction().run();
/* Signal the current selection to the newly created graph */
LamiSelectionUpdateSignal signal = new LamiSelectionUpdateSignal(LamiReportViewTabPage.this,
fSelectionIndexes, this);
TmfSignalManager.dispatchSignal(signal);
}
// ------------------------------------------------------------------------
// Signals
// ------------------------------------------------------------------------
/**
* Signal handler for selection update.
* Propagate a TmfSelectionRangeUpdatedSignal if possible.
*
* @param signal
* The selection update signal
*/
@TmfSignalHandler
public void updateSelection(LamiSelectionUpdateSignal signal) {
LamiResultTable table = fResultTable;
Object source = signal.getSource();
/*
* Don't forward signals from other tab pages, especially those
* from other views/tab page.
*/
if (this != signal.getSignalKey() ||
this == source ||
source instanceof LamiReportViewTabPage) {
/* The signal is not for us */
return;
}
Set<Integer> entryIndex = signal.getEntryIndex();
/*
* Since most of the external viewer deal only with continuous timerange and do not allow multi time range
* selection simply signal only when only one selection is present.
*/
if (entryIndex.isEmpty()) {
/*
* In an ideal world we would send a null signal to reset all view
* and simply show no selection. But since this is Tracecompass
* there is no notion of "unselected state" in most of the viewers so
* we do not update/clear the last timerange and show false information to the user.
*/
return;
}
if (entryIndex.size() == 1) {
int index = Iterables.getOnlyElement(entryIndex).intValue();
LamiTimeRange timeRange = table.getEntries().get(index).getCorrespondingTimeRange();
if (timeRange != null) {
/* Send Range update to other views */
// TODO: Consider low and high limits of timestamps here.
Number tsBeginValueNumber = timeRange.getBegin().getValue();
Number tsEndValueNumber = timeRange.getEnd().getValue();
if (tsBeginValueNumber != null && tsEndValueNumber != null) {
ITmfTimestamp start = TmfTimestamp.fromNanos(tsBeginValueNumber.longValue());
ITmfTimestamp end = TmfTimestamp.fromNanos(tsEndValueNumber.longValue());
TmfSignalManager.dispatchSignal(new TmfSelectionRangeUpdatedSignal(LamiReportViewTabPage.this, start, end));
}
}
}
fSelectionIndexes = entryIndex;
}
/**
* Signal handler for time range selections
*
* @param signal
* The received signal
*/
@TmfSignalHandler
public void externalUpdateSelection(TmfSelectionRangeUpdatedSignal signal) {
LamiResultTable table = fResultTable;
if (signal.getSource() == this) {
/* We are the source */
return;
}
TmfTimeRange range = new TmfTimeRange(signal.getBeginTime(), signal.getEndTime());
Set<Integer> selections = getIndexOfEntriesIntersectingTimerange(table, range);
/* Update all LamiViewer */
LamiSelectionUpdateSignal signal1 = new LamiSelectionUpdateSignal(LamiReportViewTabPage.this, selections, this);
TmfSignalManager.dispatchSignal(signal1);
}
private static Set<Integer> getIndexOfEntriesIntersectingTimerange(LamiResultTable table, TmfTimeRange range) {
Set<Integer> selections = new HashSet<>();
for (LamiTableEntry entry : table.getEntries()) {
LamiTimeRange timerange = entry.getCorrespondingTimeRange();
if (timerange == null) {
/* Return since the table have no timerange */
return selections;
}
// TODO: Consider low and high limits of timestamps here.
Number tsBeginValueNumber = timerange.getBegin().getValue();
Number tsEndValueNumber = timerange.getEnd().getValue();
if (tsBeginValueNumber != null && tsEndValueNumber != null) {
ITmfTimestamp start = TmfTimestamp.fromNanos(tsBeginValueNumber.longValue());
ITmfTimestamp end = TmfTimestamp.fromNanos(tsEndValueNumber.longValue());
TmfTimeRange tempTimeRange = new TmfTimeRange(start, end);
if (tempTimeRange.getIntersection(range) != null) {
selections.add(table.getEntries().indexOf(entry));
}
}
}
return selections;
}
}