nl.strohalm.cyclos.controls.reports.statistics.graphs.StatisticalDataProducer.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.controls.reports.statistics.graphs.StatisticalDataProducer.java

Source

/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
    
Cyclos 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 2 of the License, or
(at your option) any later version.
    
Cyclos 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 Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    
 */
package nl.strohalm.cyclos.controls.reports.statistics.graphs;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;

import nl.strohalm.cyclos.controls.ActionContext;
import nl.strohalm.cyclos.entities.reports.StatisticalNumber;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.services.stats.StatisticalResultDTO;
import nl.strohalm.cyclos.services.stats.general.FilterUsed;
import nl.strohalm.cyclos.utils.conversion.KeyToHelpNameConverter;

import org.jfree.chart.plot.Marker;
import org.jfree.data.Values2D;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.general.Dataset;
import org.jfree.data.general.PieDataset;

import de.laures.cewolf.DatasetProduceException;
import de.laures.cewolf.DatasetProducer;
import de.laures.cewolf.tooltips.CategoryToolTipGenerator;
import de.laures.cewolf.tooltips.PieToolTipGenerator;

/**
 * A wrapper class around the data value object called StatisticalResultDTO, adding functionality for rendering graphs with CeWolf. If showTable and
 * showGraph are both false, a small table is shown indicating too little data is available.
 * <p>
 * The help file is abstracted from the baseKey. If "bla.blah.blaaah" is the baseKey, then the appropriate helpfile will be blaBlahBlaaah.jsp. This is
 * the helpfile for the table, or for the graph in case no table is shown. The help for the graph in case a table is shown too, is not determined by
 * this class; it is a general file which is set in the results jsp.
 * 
 * @author rinke
 */
public class StatisticalDataProducer
        implements Serializable, DatasetProducer, CategoryToolTipGenerator, PieToolTipGenerator {

    private static final long serialVersionUID = 3323675832104771077L;

    /**
     * the dataset for the graph. If this is null, the graph hasn't been produced yet
     */
    protected Dataset dataset;
    /**
     * an unique identifier for Cewolf.
     */
    private final String producerId;
    /**
     * the valueObject with the data
     */
    private final StatisticalResultDTO resultObject;
    /**
     * the localSettings, in order to retrieve number formatting for tooltips
     */
    private LocalSettings settings;
    /**
     * the actionContext is needed to get language resource bundles
     */
    private ActionContext context;
    /**
     * rowHeaders are the final Strings of the headers of the table rows. This class changes any header resource bundle keys into the final Strings
     * needed.
     */
    private String[] rowHeaders;
    /**
     * the graph categories along the x-axis. This corresponds (usually, but not always) to the tables row headers, only that this is a shorter
     * version (as there is usually less space)
     */
    private final String[] categories;
    /**
     * the table column headers.
     */
    private final String[] columnHeaders;
    /**
     * the title above the Graph
     */
    private String title;
    /**
     * the string along the x-axis of the graph
     */
    private String x_axis;
    /**
     * the string along the y-axis of the graph
     */
    private String y_axis;
    /**
     * the total value of all sections in a pie chart;
     */
    private Double totalForPie;

    /**
     * a factor (in Math.pow(1000,scaleFactor)) for scaling the y-axis
     */
    private byte scaleFactor;

    // ######################## CONSTRUCTOR ###############################################33
    // ########################################################################################

    /**
     * This constructor wraps a dataProducer around the raw DTO object
     * @param valueObject the raw DTO data object
     * @param context needed to get the language resource bundle, in order to set table headers and graph strings
     */
    public StatisticalDataProducer(final StatisticalResultDTO valueObject, final ActionContext context) {
        resultObject = valueObject;
        producerId = "graphProducer";
        final int rows = valueObject.getTableCells().length;
        categories = new String[resultObject.getCategoriesCount()];
        rowHeaders = new String[rows];
        final int columns = (rows > 0) ? valueObject.getTableCells()[0].length : 0;
        columnHeaders = new String[columns];
        setResourceStrings(context);
        /*
         * The calling of setDataset is complicated, especially for child classes. It MUST be called in the constructor, and cannot be called at first
         * prior to returning the dataset in produceDataset, because in the jsp cewolf first creates the axis labels before calling produceDataset. As
         * setDataset also performs the data scaling, the axis labels would not be scaled because they would then be created before scaling occurs. As
         * the constructor for a child class MUST call the super constructor in its first line, setDataset would be run before the droppedColumns can
         * be set. This would create wrong scaling also based on columns to be dropped. Therefore, in this constructor, setDataset may only be called
         * for StatisticalDataproducer itself, not for its child classes. The child classes MUST call setDataset AFTER setting the number of columns
         * to drop, but still INSIDE their constructor.
         */
        if (this.getClass() == StatisticalDataProducer.class) {
            setDataset();
        }
    }

    /**
     * This private constructor is used when creating a multiple chart. See the {@link #getMultiGraphProducers} method.
     * 
     * @param original the original statisticalDataProducer from which this constructor is called.
     * @param index the index of this producer in the array of producers
     * @param numberOfPoints the number of points (categories) to show in the graph.
     */
    private StatisticalDataProducer(final StatisticalDataProducer original, final int index,
            final int numberOfPoints) {
        final boolean byColumn = original.resultObject.getMultiGraph() == StatisticalResultDTO.MultiGraph.BY_COLUMN;
        final Number[][] data = new Number[numberOfPoints][1];
        for (int i = 0; i < numberOfPoints; i++) {
            data[i][0] = byColumn ? original.getTableCells()[i][index] : original.getTableCells()[index][i];
        }
        resultObject = new StatisticalResultDTO(data);
        resultObject.setGraphType(original.resultObject.getGraphType());
        // any column subHeaders are passed as units for the y-axis, but the parenthesis must be removed.
        String units = original.getColumnSubHeaders()[index];
        if (units.indexOf("(") == 0 && units.lastIndexOf(")") == units.length() - 1) {
            units = units.substring(1, units.length() - 1);
        }
        resultObject.setYAxisUnits(units);
        producerId = "graphProducer";
        categories = new String[numberOfPoints];
        System.arraycopy(byColumn ? original.categories : original.getColumnHeaders(), 0, categories, 0,
                numberOfPoints);
        columnHeaders = new String[1];
        columnHeaders[0] = byColumn ? original.columnHeaders[index] : original.categories[index];
        final int numberOfGraphs = byColumn ? original.columnHeaders.length : original.categories.length;
        x_axis = (index == numberOfGraphs - 1) ? original.getX_axis() : "";
        y_axis = byColumn ? original.columnHeaders[index] : original.categories[index];
        settings = original.settings;
        setDataset();
    }

    /**
     * Cewolf needs this method for generation of tooltips when you hoover the mouse over a graph bar or point If the settings are available, use
     * them, if not, then unformatted numbers will be shown As tooltips are not essential, and as generation of them should never allow the rendering
     * of graphs to fail due to exceptions, all is inside a try catch block.
     */
    @Override
    public String generateToolTip(final CategoryDataset lDataset, final int series, final int lCategories) {
        try {
            final Number number = lDataset.getValue(series, lCategories);
            try {
                final byte precision = (number instanceof StatisticalNumber)
                        ? ((StatisticalNumber) number).getPrecision()
                        : 0;
                if (settings != null) {
                    final BigDecimal value = (new BigDecimal(1000).pow(scaleFactor))
                            .multiply(new BigDecimal(number.floatValue()));
                    final String result = settings.getNumberConverterForPrecision(precision).toString(value);
                    return result;
                }
            } catch (final Exception e) {
                // if anything goes wrong, do nothing but continue with String.valueOf
            }
            return String.valueOf(number.doubleValue());
        } catch (final Exception e) {
            // if all failed, just return no tooltips
            return "";
        }
    }

    /**
     * Cewolf needs this method for generation of tooltips when you hoover the mouse over a Pie graph section. If the settings are available, use
     * them, if not, then unformatted numbers will be shown. As tooltips are not essential, and as generation of them should never allow the rendering
     * of graphs to fail due to exceptions, all is inside a try catch block.
     * 
     * @param dataset a <code>PieDataset</code>
     * @param key for identifying the section.
     * @param pieIndex in case of multiple Pie charts. Not used in cyclos.
     */
    @Override
    @SuppressWarnings("rawtypes")
    public String generateToolTip(final PieDataset dataset, final Comparable key, final int pieIndex) {
        try {
            final Number number = dataset.getValue(key);
            try {
                final byte precision = (number instanceof StatisticalNumber)
                        ? ((StatisticalNumber) number).getPrecision()
                        : 0;
                if (settings != null) {
                    final BigDecimal value = (new BigDecimal(1000).pow(scaleFactor))
                            .multiply(new BigDecimal(number.floatValue()));
                    final int percentage = (int) Math
                            .round(value.divide(new BigDecimal(getTotalForPie())).doubleValue() * 100);
                    final String result = settings.getNumberConverterForPrecision(precision).toString(value) + " (="
                            + percentage + "%)";
                    return result;
                }
            } catch (final Exception e) {
                // if anything goes wrong, do nothing but continue with String.valueOf
            }
            final String result = String.valueOf(number.doubleValue());
            final int percentage = (int) Math.round(100 * (number.doubleValue() / getTotalForPie()));
            return result + " (=" + percentage + "%)";
        } catch (final Exception e) {
            // if all failed, just return no tooltips
            return "";
        }
    }

    public String getBaseKey() {
        return resultObject.getBaseKey();
    }

    public String[] getColumnHeaders() {
        return columnHeaders;
    }

    public String[] getColumnSubHeaders() {
        return resultObject.getColumnSubHeaders();
    }

    /**
     * gets the domain markers for the graph from the encapsulated data object
     */
    public Marker[] getDomainMarkers() {
        return resultObject.getDomainMarkers();
    }

    /**
     * gets the number of filter specs to be displayed with this data.
     * 
     * @return the number of filters mentioned in the "filters used" box below the graph or table
     */
    public int getFilterCount() {
        return resultObject.getFiltersUsed().size();
    }

    /**
     * gets the list with used Filters, to be shown below graph or table (if anything is in it). It manipulates the list also in the following ways:
     * <ul>
     * <li>Changes keys to Strings by using the language resource bundle
     * <li>fills up the list of values with "" so all columns have equal length.
     * </ul>
     * 
     * @return a <code>List</code> with <code>FilterUsed</code> objects, one for each relevant filter.
     */
    public List<FilterUsed> getFiltersUsed() {
        final List<FilterUsed> filterList = resultObject.getFiltersUsed();
        // determine max size
        int maxSize = 0;
        for (final FilterUsed filterUsed : filterList) {
            final int size = filterUsed.getValues().size();
            if (size > maxSize) {
                maxSize = size;
            }
        }
        for (final FilterUsed filterUsed : filterList) {
            // change keys with Strings
            if (filterUsed.getFilterUse() == FilterUsed.FilterUse.NO_SELECT
                    || filterUsed.getFilterUse() == FilterUsed.FilterUse.NOT_USED) {
                for (final String key : filterUsed.getValues()) {
                    filterUsed.changeKeyToValue(key, context.message(key));
                }
            }
            // fill up with "" if needed
            final int size = filterUsed.getValues().size();
            filterUsed.addBlankRows(maxSize - size);
        }
        return filterList;
    }

    public String getGraphTypeValue() {
        return resultObject.getGraphType().getValue();
    }

    /**
     * the height of the graph. This is dynamic because in case of long series, the legend tends to eat up all available space, resulting in a huge
     * legend and a small graph.
     * 
     * @return an int which is the graph height. This is read by the jsp.
     */
    public int getHeight() {
        if (getShowLegend().equals("false")) {
            return 300;
        }
        return (int) Math.round(300 + (getRowHeaders().length * 150.0 / 20.0));
    }

    /**
     * the help anchor (=name of the #anchor in the help file), generated from the basekey. If the basekey is one.two.three, the helpName will be
     * oneTwoThree.
     */
    public String getHelpAnchor() {
        final KeyToHelpNameConverter converter = new KeyToHelpNameConverter();
        return converter.toString(getBaseKey());
    }

    public String getHelpFile() {
        return resultObject.getHelpFile();
    }

    /**
     * this method takes the object (this), and splits the data up into an array of separated StatisticalDataProducers, where each serie or category
     * in the original object gets a separate StatisticalDataProducer in the resulting array. So, in case of a StatisticalDataProducer object with 4
     * series, the result of this method will be an array with 4 StatisticalDataProducerObjects, each of them having one of the original series as a
     * single series in its data. This method is needed to be able to produce a combined graph. A combined graph is useful in case the y-axis ranges
     * of the different series can not reasonably be combined in one single graph with a common y-axis (for example: if series 1 ranges from y=100.000
     * to y=200.000, and series 2 ranges from y=1 to y=10.) Graphs can be split into multiple graphs by series, or by categories. In the latter case,
     * for each category a separate graph is created.
     * <p>
     * <b>Notes:</b><br>
     * The following fields of the resultObject are treated as follows:
     * <ul>
     * <li>The <code>dontSwitchXY</code> boolean field is ignored by this method.
     * <li>The <code>seriesCount</code> field is ignored too, as each graph has per definition one series.
     * <li>The <code>categoriesCount</code> field sets the number of datapoints on the x-axis. Not setting this just shows all points.
     * </ul>
     * 
     * @return an array of separated StatisticalDataProducers, where each serie or category in the original object gets a separate
     * StatisticalDataProducer in the resulting array
     */
    public StatisticalDataProducer[] getMultiGraphProducers() {
        if (!isMultiGraph()) {
            return null;
        }
        final int numberOfGraphs;
        if (resultObject.getMultiGraph() == StatisticalResultDTO.MultiGraph.BY_COLUMN) {
            numberOfGraphs = getColumnHeaders().length;
        } else {
            numberOfGraphs = getRowHeaders().length;
        }
        final StatisticalDataProducer[] producerArray = new StatisticalDataProducer[numberOfGraphs];
        for (int i = 0; i < numberOfGraphs; i++) {
            final StatisticalDataProducer item = new StatisticalDataProducer(this, i,
                    resultObject.getCategoriesCount());
            producerArray[i] = item;
        }
        return producerArray;
    }

    /**
     * This method is needed for Cewolf to produce the graphs. See remarks at the hasExpired method.
     */
    @Override
    public String getProducerId() {
        return producerId;
    }

    public String[] getRowHeaders() {
        return rowHeaders;
    }

    /**
     * determines if the legend is shown or not. It is always shown by the pie charts. If a bar or line, then it is shown if there are more than one
     * series to be displayed. This method is read by the jsp.
     * 
     * @return a jsp readable string which can either be "true" or "false".
     */
    public String getShowLegend() {
        if (resultObject.getGraphType() == StatisticalResultDTO.GraphType.PIE) {
            return "true";
        }
        final Values2D values2D = (Values2D) dataset;
        if (values2D.getRowCount() == 1) {
            return "false";
        }
        return "true";
    }

    /**
     * the subtitle for the graph
     */
    public String getSubTitle() {
        final String subTitle = resultObject.getSubTitle();
        if (subTitle == null) {
            return "";
        }
        return subTitle;
    }

    /**
     * gets the raw data
     * @return a 2 dimensional array of <code>Number</code>s with the raw data
     */
    public Number[][] getTableCells() {
        return resultObject.getTableCells();
    }

    // derived getters and setters, getting their values from the wrapped valueObject

    public int getTableColumnCount() {
        return getColumnHeaders().length + 1;
    }

    public String getTitle() {
        if (title == null && context != null) {
            return context.message(getBaseKey());
        }
        return title;
    }

    /**
     * gets the X-axis label. If the language resource key for this label was not defined, it returns an empty string. It adjusts the label
     * automatically for any scaling done: if so, the scalefactor is appended to the label, between parenthesis (like "( x 1000)")
     * @return the x-axis label of the graph
     */
    public String getX_axis() {
        if (x_axis.startsWith("???") & x_axis.endsWith("???")) {// in case of key not defined
            return "";
        }
        final StringBuilder sb = new StringBuilder();
        sb.append(x_axis);
        if (resultObject.getScaleFactorX() == null) {
            if (resultObject.getXAxisUnits().length() > 0) {
                sb.append("  ").append("(").append(resultObject.getXAxisUnits()).append(")");
            }
            return sb.toString();
        }
        sb.append("   ").append(resultObject.getScaleFactorX());
        if (resultObject.getXAxisUnits().length() > 0) {
            sb.insert(sb.length() - 1, " ").insert(sb.length() - 1, resultObject.getXAxisUnits());
        }
        return sb.toString();

    }

    /**
     * gets the y-axis label. The label is adjusted for automatic scaling: if scaling was applied, the scalefactor is appended between parenthesis
     * (like "(x 1000)"). If units were used, the units are placed behind the scale factor number, inside the parenthesis, so like "(x 1000 units)".
     * @return the y-axis label for the graph.
     */
    public String getY_axis() {
        if (scaleFactor == 0) {
            final StringBuilder sb = new StringBuilder();
            sb.append(y_axis);
            if (resultObject.getYAxisUnits().length() > 0) {
                sb.append("  ").append("(").append(resultObject.getYAxisUnits()).append(")");
            }
            return sb.toString();
        }
        // apply scaling suffix
        final String factorSign = (scaleFactor < 0) ? resultObject.getYAxisUnits() + "/" : "x";
        final int factor = (int) Math.pow(1000, Math.abs(scaleFactor));
        final StringBuilder sb = new StringBuilder();
        if (settings != null) {
            sb.append(settings.getNumberConverterForPrecision(0).toString(new BigDecimal(factor)));
        } else {
            sb.append(String.valueOf(factor));
        }
        sb.insert(0, factorSign).insert(0, "   (");
        if (scaleFactor > 0 && resultObject.getYAxisUnits().length() > 0) {
            sb.append(" ").append(resultObject.getYAxisUnits());
        }
        sb.append(")");
        sb.insert(0, y_axis);
        return sb.toString();
    }

    /**
     * This method is needed for Cewolf to produce the graphs. It always returns true. If not, DataProducers with equal producerId's return the same
     * chart (as it is cached). As ProducerId is often equal for a lot of graphs, hasExpired is always set to true.
     */
    @Override
    @SuppressWarnings("rawtypes")
    public boolean hasExpired(final Map arg0, final Date arg1) {
        return true;
    }

    public boolean isMultiGraph() {
        return resultObject.getMultiGraph() != StatisticalResultDTO.MultiGraph.NONE;
    }

    /**
     * if this is false, the applied filters are not shown in the result. The default is true.
     */
    public boolean isShowAppliedFilters() {
        return resultObject.getFiltersUsed().size() > 0;
    }

    public boolean isShowGraph() {
        return resultObject.getGraphType() != StatisticalResultDTO.GraphType.NONE;
    }

    public boolean isShowTable() {
        return resultObject.isShowTable();
    }

    /**
     * This method is needed for creating graphs with Cewolf.
     */
    @Override
    @SuppressWarnings("rawtypes")
    public Object produceDataset(final Map params) throws DatasetProduceException {
        return dataset;
    }

    public void setSettings(final LocalSettings settings) {
        this.settings = settings;
    }

    /**
     * gets the value of resultObject.hasErrorBars() for descendant classes
     * @return true if there are error bars defined
     */
    protected boolean hasErrorBars() {
        return resultObject.hasErrorBars();
    }

    /**
     * sets the dataset for the graph. Takes the following actions:
     * <ul>
     * <li>checks if a graph is needed
     * <li>it may switch x and y, depending on the <code>resultObject</code>'s <code>dontSwitchXY</code> property
     * <li>
     * generates the dataset
     * <li>scales it.
     * </ul>
     */
    protected void setDataset() {
        if (resultObject.getGraphType() == StatisticalResultDTO.GraphType.PIE) {
            dataset = GraphDatasetGenerator.generatePieDataset(getRowHeaders(), getTableCells());
            return;
        }
        if ((isShowGraph()) && (!isMultiGraph())) {
            final String[] seriesNames;
            final boolean errorBars = resultObject.hasErrorBars();
            final Number[][] dataArray;
            if (resultObject.isDontSwitchXY()) {
                // dont switch, exceptional behavior
                seriesNames = GraphDatasetGenerator.createGraphSeries(getRowHeaders(),
                        resultObject.getSeriesCount());
                dataArray = getTableCells();
            } else {
                // switch, default behavior
                seriesNames = GraphDatasetGenerator.createGraphSeries(getColumnHeaders(),
                        resultObject.getSeriesCount());
                dataArray = GraphDatasetGenerator.switchXYData(getTableCells());
            }
            final CategoryDataset dataset = GraphDatasetGenerator.generateDataset(dataArray, seriesNames,
                    categories, errorBars);
            scaleFactor = calculateScaleFactor(dataset);
            this.dataset = GraphDatasetGenerator.scaleData(dataset, scaleFactor, errorBars);
        }
    }

    /**
     * method calculates the scalefactor from the input dataset. Scaling means for example: if the numbers are in the range of millions (so: from 0 to
     * 10 million), numbers 1 to 10 are shown on the y-axis, and the y-axis label indicates " (x 1.000.000)". Scaling is done per thousands.
     * 
     * @param dataset
     * @return the scaleFactor. A value of 1 correspondents with "x1000", a value of 2 with "x1.000.000".
     */
    private byte calculateScaleFactor(final Object dataset) {
        if (isShowGraph() && (!isMultiGraph())) {
            return GraphDatasetGenerator.calculateScaleFactor(dataset);
        }
        return (byte) 0;
    }

    /**
     * gets the sum value of all pie sections, in order to calculate percentages in tooltips.
     * @return a double which is the total value of all sections of the pie.
     */
    private double getTotalForPie() {
        if (totalForPie == null) {
            double result = 0;
            for (int i = 0; i < resultObject.getTableCells().length; i++) {
                result += resultObject.getTableCells()[i][0].doubleValue();
            }
            totalForPie = result;
        }
        return totalForPie.doubleValue();
    }

    /**
     * Takes the keys, and uses these to look up the real strings in the language resource bundle. sets the title, the x- and y-axis labels, and the
     * categories. This should always be called from the constructor.
     * 
     * @param context
     */
    private void setResourceStrings(final ActionContext context) {
        this.context = context;
        title = context.message(getBaseKey());
        x_axis = context.message(getBaseKey() + ".xAxis");
        y_axis = context.message(getBaseKey() + ".yAxis");
        for (int i = 0; i < rowHeaders.length; i++) {
            if (resultObject.getRowHeader(i) != null) {
                rowHeaders[i] = resultObject.getRowHeader(i);
                if (!resultObject.isDontSwitchXY() && (i < resultObject.getCategoriesCount())) {
                    categories[i] = rowHeaders[i];
                }
            } else {
                rowHeaders[i] = context.message(resultObject.getRowKey(i), resultObject.getRowKeyArgs(i));
                if (!resultObject.isDontSwitchXY() && (i < resultObject.getCategoriesCount())) {
                    categories[i] = context.message(resultObject.getRowKey(i) + ".short");
                    if (categories[i].startsWith("???") & categories[i].endsWith("???")) {// in case of key not defined
                        categories[i] = "";
                    }
                }
            }
        }
        for (int i = 0; i < columnHeaders.length; i++) {
            if (resultObject.getColumnHeader(i) != null) {
                columnHeaders[i] = resultObject.getColumnHeader(i);
                if (resultObject.isDontSwitchXY() && (i < resultObject.getCategoriesCount())) {
                    categories[i] = columnHeaders[i];
                }
            } else {
                columnHeaders[i] = context.message(resultObject.getColumnKey(i));
                if (resultObject.isDontSwitchXY() && (i < resultObject.getCategoriesCount())) {
                    categories[i] = context.message(resultObject.getColumnKey(i) + ".short");
                    if (categories[i].startsWith("???") & categories[i].endsWith("???")) {// in case of key not defined
                        categories[i] = "";
                    }
                }
            }
        }
    }

}