package com.griddynamics.jagger.webclient.client.components;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.googlecode.gflot.client.DataPoint;
import com.googlecode.gflot.client.Series;
import com.googlecode.gflot.client.SeriesData;
import com.googlecode.gflot.client.SeriesHandler;
import com.googlecode.gflot.client.SimplePlot;
import com.googlecode.gflot.client.options.PointsSeriesOptions;
import com.griddynamics.jagger.dbapi.dto.PlotSingleDto;
import com.griddynamics.jagger.dbapi.dto.PointDto;
import com.griddynamics.jagger.dbapi.model.AbstractIdentifyNode;
import com.griddynamics.jagger.dbapi.model.LegendNode;
import com.sencha.gxt.core.client.ValueProvider;
import com.sencha.gxt.core.client.util.Format;
import com.sencha.gxt.data.shared.ModelKeyProvider;
import com.sencha.gxt.data.shared.TreeStore;
import com.sencha.gxt.widget.core.client.tips.QuickTip;
import java.util.ArrayList;
import java.util.List;
import static com.googlecode.gflot.client.options.PointsSeriesOptions.PointSymbol.CIRCLE;
import static com.googlecode.gflot.client.options.PointsSeriesOptions.PointSymbol.CROSS;
import static com.googlecode.gflot.client.options.PointsSeriesOptions.PointSymbol.DIAMOND;
import static com.googlecode.gflot.client.options.PointsSeriesOptions.PointSymbol.SQUARE;
import static com.googlecode.gflot.client.options.PointsSeriesOptions.PointSymbol.TRIANGLE;
/**
* Implementation of AbstractTree that represents interactive legend as tree.
*/
public class LegendTree extends AbstractTree<AbstractIdentifyNode, LegendTree.CellData> {
/**
* Plot that would controlled with Legend tree
*/
private SimplePlot plot;
/**
* null if it is metric, not null if it is trend
*/
// todo : JFG-803 simplify trends plotting mechanism
private List<Integer> trendSessionIds;
/**
* Plots panel where plot is situated
*/
private PlotsPanel plotsPanel;
private final static ValueProvider<AbstractIdentifyNode, CellData> VALUE_PROVIDER = new LegendTreeValueProvider();
/**
* Constructor matches super class
*
* @param trendSessionIds - set null if it is metric, sorted list of session ids if it is trend
*/
public LegendTree(SimplePlot plot, PlotsPanel plotsPanel, List<Integer> trendSessionIds) {
super(
new TreeStore<AbstractIdentifyNode>(new ModelKeyProvider<AbstractIdentifyNode>() {
@Override
public String getKey(AbstractIdentifyNode item) {
return item.getId();
}
}),
VALUE_PROVIDER);
this.plot = plot;
this.plotsPanel = plotsPanel;
this.trendSessionIds = trendSessionIds;
this.setAutoExpand(true);
this.setCell(new LegendNodeCell());
this.setSelectionModel(null);
// register tip manager for tree
QuickTip qt = new QuickTip(this);
qt.setShadow(false);
}
@Override
protected void check(AbstractIdentifyNode item, CheckState state) {
noRedrawCheck(item, state);
redrawPlot();
}
/**
* Check all items in tree
*/
public void checkAll() {
for (AbstractIdentifyNode node : store.getRootItems()) {
setChecked(node, CheckState.CHECKED);
}
}
/**
* Adds or removes lines without redrawing plot. Changes can't be seen.
* If group item was checked, we want to redraw plot once (instead of firing redrawing for each line).
*
* @param item chosen item
* @param state check state
*/
private void noRedrawCheck(AbstractIdentifyNode item, CheckState state) {
PlotSingleDto plotSingleDto = null;
if (item instanceof LegendNode) {
plotSingleDto = ((LegendNode) item).getLine();
}
if (plotSingleDto != null) {
if (state == CheckState.CHECKED) {
Series series = Series.create()
.setId(item.getId())
.setColor(plotSingleDto.getColor())
.setLabel(plotSingleDto.getLegend());
PointsSeriesOptions pointsSeriesOptions;
switch (plotSingleDto.getPointShape()) {
case CIRCLE_EMPTY: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(CIRCLE).setFill(false); break;
case CIRCLE_FILLED: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(CIRCLE).setFill(true); break;
case CROSS_EMPTY: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(CROSS).setFill(false); break;
case CROSS_FILLED: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(CROSS).setFill(true); break;
case DIAMOND_EMPTY: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(DIAMOND).setFill(false); break;
case DIAMOND_FILLED: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(DIAMOND).setFill(true); break;
case SQUARE_EMPTY: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(SQUARE).setFill(false); break;
case SQUARE_FILLED: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(SQUARE).setFill(true); break;
case TRIANGLE_EMPTY: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(TRIANGLE).setFill(false); break;
case TRIANGLE_FILLED: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(TRIANGLE).setFill(true); break;
default: pointsSeriesOptions = PointsSeriesOptions.create().setSymbol(CIRCLE).setFill(false); break;
}
series.setPointsOptions(pointsSeriesOptions.setRadius(2));
SeriesHandler sh = plot.getModel().addSeries(series);
for (PointDto point : plotSingleDto.getPlotData()) {
if (trendSessionIds != null) {
// it is trend
sh.add(DataPoint.of(trendSessionIds.indexOf((int) point.getX()), point.getY()));
} else {
sh.add(DataPoint.of(point.getX(), point.getY()));
}
}
} else if (state == CheckState.UNCHECKED) {
// remove curve from view
JsArray<Series> seriesArray = plot.getModel().getSeries();
int k;
for (k = 0; k < seriesArray.length(); k++) {
Series curSeries = seriesArray.get(k);
// label used as id
if (curSeries.getId().equals(item.getId())) {
// found
plot.getModel().removeSeries(k);
break;
}
}
}
} else {
for (AbstractIdentifyNode child : store.getAllChildren(item)) {
noRedrawCheck(child, state);
}
}
}
/**
* Redraw plot with specific axis ranges
*/
private void redrawPlot() {
if (!plotsPanel.isEmpty()) {
double minXVisible = plotsPanel.getMinXAxisVisibleValue();
double maxXVisible = plotsPanel.getMaxXAxisVisibleValue();
if (plot.isAttached()) {
double minYVisible = plot.getAxes().getY().getMinimumValue();
// This multiplication is needed to leave some free space (10% of Y-axis length) above line on the plot
double maxYVisible = calculateMaxYAxisValue(plot) * 1.1;
// If max value is too small it won't be visible on the plot, since the smallest float number that GWT can show is 10^-300
Double minVisibleFloatValue = Math.pow(10, -300);
if (maxYVisible < minVisibleFloatValue) maxYVisible = minVisibleFloatValue;
// save y axis range for plot from very start
plot.getOptions().getYAxisOptions().setMinimum(minYVisible).setMaximum(maxYVisible);
}
// set x axis in range as all other plots
plot.getOptions().getXAxisOptions().setMinimum(minXVisible).setMaximum(maxXVisible);
plot.redraw();
}
}
private double calculateMaxYAxisValue(SimplePlot plot) {
JsArray<Series> seriesArray = plot.getModel().getSeries();
double maxValue = Double.MIN_VALUE;
for (int i = 0; i < seriesArray.length(); i++) {
// get curve
SeriesData curve = seriesArray.get(i).getData();
for (int j = 0; j < curve.length(); j++) {
double temp = curve.getY(j);
if (maxValue < temp) {
maxValue = temp;
}
}
}
return maxValue;
}
/**
* Returns list of checked lines
*
* @return list of PlotSingleDto objects
*/
public List<PlotSingleDto> getListOfSelectedLines() {
List<PlotSingleDto> result = new ArrayList<PlotSingleDto>();
for (AbstractIdentifyNode node : store.getAll()) {
if ((isChecked(node)) && (node instanceof LegendNode)) {
result.add(((LegendNode) node).getLine());
}
}
return result;
}
/**
* Model of cell to display.
*/
protected static class CellData {
private String displayName;
private String color;
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public void setColor(String color) {
this.color = color;
}
public String getDisplayName() {
return displayName;
}
public String getColor() {
return color;
}
}
/**
* Cell to be displayed with specific cell model (CellData)
*/
private static class LegendNodeCell extends AbstractCell<CellData> {
@Override
public void render(Context context, CellData value, SafeHtmlBuilder sb) {
sb.appendHtmlConstant(
(value.getColor() != null ? "<font color=\'" + value.getColor() + "\'>▄▄</font>" : "") +
"<font qtip='" + Format.htmlEncode(value.getDisplayName()) + "'> " + value.getDisplayName() + "</font>");
}
}
/**
* Value provider to set up cell model depending on node model
*/
private static class LegendTreeValueProvider implements ValueProvider<AbstractIdentifyNode, CellData> {
@Override
public CellData getValue(AbstractIdentifyNode object) {
CellData cellData = new CellData();
cellData.setDisplayName(object.getDisplayName());
if (object instanceof LegendNode) {
cellData.setColor(((LegendNode) object).getLine().getColor());
}
return cellData;
}
@Override
public void setValue(AbstractIdentifyNode object, CellData value) {
object.setDisplayName(value.getDisplayName());
}
@Override
public String getPath() {
return "legend";
}
}
}