Mercurial > dive4elements > gnv-client
diff gnv-artifacts/src/main/java/de/intevation/gnv/chart/TimeSeriesChart.java @ 1119:7c4f81f74c47
merged gnv-artifacts
author | Thomas Arendsen Hein <thomas@intevation.de> |
---|---|
date | Fri, 28 Sep 2012 12:14:00 +0200 |
parents | f953c9a559d8 |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/chart/TimeSeriesChart.java Fri Sep 28 12:14:00 2012 +0200 @@ -0,0 +1,637 @@ +/* + * Copyright (c) 2010 by Intevation GmbH + * + * This program is free software under the LGPL (>=v2.1) + * Read the file LGPL.txt coming with the software for details + * or visit http://www.gnu.org/licenses/ if it does not exist. + */ + +package de.intevation.gnv.chart; + +import de.intevation.gnv.artifacts.ressource.RessourceFactory; + +import de.intevation.gnv.geobackend.base.Result; + +import de.intevation.gnv.state.describedata.KeyValueDescibeData; + +import de.intevation.gnv.timeseries.gap.TimeGap; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.log4j.Logger; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartTheme; + +import org.jfree.chart.axis.Axis; +import org.jfree.chart.axis.DateAxis; +import org.jfree.chart.axis.DateTickUnit; +import org.jfree.chart.axis.DateTickUnitType; +import org.jfree.chart.axis.TickUnitSource; +import org.jfree.chart.axis.TickUnits; +import org.jfree.chart.axis.ValueAxis; + +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; + +import org.jfree.data.general.Series; + +import org.jfree.data.time.Minute; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; + +/** + * This class is used to create timeseries charts. The domain axis contains + * multiple date/time objects. + * + * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a> + */ +public class TimeSeriesChart +extends AbstractXYLineChart +{ + + /** + * Constant format which can be useful to format date items. Value is + * {@value}. + */ + public static final String DEFAULT_DATE_FORMAT = "dd-MMM-yyyy"; + + /** + * Constant field used if no gap detection should be done here. This field + * is used in @see #getTimeGapValue. Value is {@value}. + */ + public static final long NO_TIME_GAP = Long.MAX_VALUE - 1000; + + /** + * Percentage used for gap detection. Its value is {@value}. + */ + public static int GAP_SIZE = 5; // in percent + + /** + * Logger used for logging with log4j. + */ + private static Logger log = Logger.getLogger(TimeSeriesChart.class); + + static { + /* The percentage defining the width of a gap should be configured in + * conf.xml instead of being configured in a system property */ + GAP_SIZE = Integer.getInteger("chart.gap.percentage", GAP_SIZE); + } + + + /** + * Constructor used to create <code>TimeSeries</code> charts. + * + * @param labels Labels used to be displayed in title, subtitle and so on. + * @param theme ChartTheme used to adjust the rendering of this chart. + * @param parameters Collection containing a bunch of parameters. + * @param measurements Collection containing a bunch of measurements. + * @param dates Collection containing a bunch of date objects. + * @param result Collection containing a bunch of <code>Result</code> + * objects which contain the actual data items to be displayed. + * @param timeGaps Collection with timegap definitions. + * @param locale Locale used to specify the format of labels, numbers, ... + * @param linesVisible Render lines between data points if true, otherwise + * not. + * @param shapesVisible Render vertices as points if true, otherwise not. + */ + public TimeSeriesChart( + ChartLabels labels, + ChartTheme theme, + Collection parameters, + Collection measurements, + Collection dates, + Collection result, + Collection timeGaps, + Locale locale, + boolean linesVisible, + boolean shapesVisible + ) { + this.labels = labels; + this.theme = theme; + this.parameters = parameters; + this.measurements = measurements; + this.dates = dates; + this.resultSet = result; + this.timeGaps = timeGaps; + this.locale = locale; + this.PLOT_ORIENTATION = PlotOrientation.VERTICAL; + this.linesVisible = linesVisible; + this.shapesVisible = shapesVisible; + this.datasets = new HashMap(); + this.ranges = new HashMap(); + } + + + /** + * see de.intevation.gnv.chart.AbstractXYLineChart#initChart() + */ + @Override + protected void initChart() { + chart = ChartFactory.createTimeSeriesChart( + labels.getTitle(), + labels.getDomainAxisLabel(), + null, + null, + true, + false, + false + ); + + XYPlot plot = (XYPlot) chart.getPlot(); + plot.setDomainAxis(0, new DateAxis( + labels.getDomainAxisLabel(), TimeZone.getDefault(), locale)); + } + + + /** + * @see de.intevation.gnv.chart.AbstractXYLineChart#initData() + */ + protected void initData() { + log.debug("init data for timeseries chart"); + + String breakPoint1 = null; + String breakPoint2 = null; + String breakPoint3 = null; + + Iterator iter = resultSet.iterator(); + Result row = null; + String seriesName = null; + String parameter = null; + TimeSeries series = null; + + int idx = 0; + int startPos = 0; + int endPos = 0; + Date startDate = null; + Date endDate = null; + + Result[] results = + (Result[]) resultSet.toArray(new Result[resultSet.size()]); + + while (iter.hasNext()) { + row = (Result) iter.next(); + + // add current data to plot and prepare for next one + if (!row.getString("GROUP1").equals(breakPoint1) || + !row.getString("GROUP2").equals(breakPoint2) || + !row.getString("GROUP3").equals(breakPoint3) + ) { + log.debug("prepare data/plot for next dataset"); + + if(series != null) { + // add gaps before adding series to chart + startDate = results[startPos].getDate("XORDINATE"); + endDate = results[endPos-1].getDate("XORDINATE"); + addGaps(results,series,startDate,endDate,startPos,endPos); + addSeries(series, parameter, idx); + + startPos = endPos + 1; + } + + // prepare variables for next plot + breakPoint1 = row.getString("GROUP1"); + breakPoint2 = row.getString("GROUP2"); + breakPoint3 = row.getString("GROUP3"); + + seriesName = createSeriesName( + breakPoint1, + breakPoint2, + breakPoint3 + ); + parameter = findParameter(seriesName); + + log.debug("next dataset is '" + seriesName + "'"); + series = new TimeSeries(seriesName, Minute.class); + } + + addValue(row, series); + storeMaxRange(ranges, row.getDouble("YORDINATE"), parameter); + endPos++; + } + + if (startPos < results.length && endPos-1 < results.length) { + // add the last dataset if existing to plot and prepare its axis + startDate = results[startPos].getDate("XORDINATE"); + endDate = results[endPos-1].getDate("XORDINATE"); + addGaps(results, series, startDate, endDate, startPos, endPos); + addSeries(series, parameter, idx); + } + + addDatasets(); + } + + + /** + * @see de.intevation.gnv.chart.AbstractXYLineChart#addValue(Result, Series) + */ + protected void addValue(Result row, Series series) { + ((TimeSeries) series).addOrUpdate( + new Minute(row.getDate("XORDINATE")), + row.getDouble("YORDINATE") + ); + } + + + /** + * @param parameter + * @see de.intevation.gnv.chart.AbstractXYLineChart#addSeries(Series, + * String, int) + */ + protected void addSeries(Series series, String parameter, int idx) { + log.debug("add series (" + parameter + ")to timeseries chart"); + + if (series == null) { + log.warn("no data to add"); + return; + } + + TimeSeriesCollection tsc = null; + + if (datasets.containsKey(parameter)) + tsc = (TimeSeriesCollection) datasets.get(parameter); + else + tsc = new TimeSeriesCollection(); + + tsc.addSeries((TimeSeries) series); + datasets.put(parameter, tsc); + } + + + /** + * Method to add processed datasets to plot. Each dataset is adjusted using + * <code>prepareAxis</code> and <code>adjustRenderer</code> methods. + */ + protected void addDatasets() { + Iterator iter = parameters.iterator(); + XYPlot plot = chart.getXYPlot(); + int idx = 0; + + TimeSeriesCollection tsc = null; + KeyValueDescibeData data = null; + String key = null; + while (iter.hasNext()) { + data = (KeyValueDescibeData) iter.next(); + key = data.getValue(); + + if (datasets.containsKey(key)) { + tsc = (TimeSeriesCollection)datasets.get(key); + plot.setDataset(idx, tsc ); + log.debug("Added " + key + " parameter to plot."); + prepareAxis(key, idx); + adjustRenderer( + idx++, + tsc.getSeriesCount(), + linesVisible, + shapesVisible + ); + } + } + } + + + /** + * @param locale + * @see de.intevation.gnv.chart.AbstractXYLineChart#localizeDomainAxis(Axis, + * Locale) + */ + protected void localizeDomainAxis(Axis axis, Locale locale) { + ((ValueAxis)axis).setStandardTickUnits(createStandardDateTickUnits( + TimeZone.getDefault(), + locale)); + } + + + /** + * @param zone + * @param locale + * @return TickUnitSource + * @see org.jfree.chart.axis.DateAxis#createStandardDateTickUnits(TimeZone, + * Locale) + */ + public static TickUnitSource createStandardDateTickUnits( + TimeZone zone, + Locale locale) + { + /* + * This method have been copied from JFreeChart's DateAxis class. + * DateFormat objects are hard coded in DateAxis and cannot be adjusted. + */ + if (zone == null) { + throw new IllegalArgumentException("Null 'zone' argument."); + } + if (locale == null) { + throw new IllegalArgumentException("Null 'locale' argument."); + } + TickUnits units = new TickUnits(); + + // date formatters + DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS", locale); + DateFormat f2 = new SimpleDateFormat("HH:mm:ss", locale); + DateFormat f3 = new SimpleDateFormat("HH:mm", locale); + DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm", locale); + DateFormat f5 = new SimpleDateFormat("d-MMM yyyy", locale); + DateFormat f6 = new SimpleDateFormat("MMM-yyyy", locale); + DateFormat f7 = new SimpleDateFormat("yyyy", locale); + + f1.setTimeZone(zone); + f2.setTimeZone(zone); + f3.setTimeZone(zone); + f4.setTimeZone(zone); + f5.setTimeZone(zone); + f6.setTimeZone(zone); + f7.setTimeZone(zone); + + // milliseconds + units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 1, f1)); + units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 5, + DateTickUnitType.MILLISECOND, 1, f1)); + units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 10, + DateTickUnitType.MILLISECOND, 1, f1)); + units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 25, + DateTickUnitType.MILLISECOND, 5, f1)); + units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 50, + DateTickUnitType.MILLISECOND, 10, f1)); + units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 100, + DateTickUnitType.MILLISECOND, 10, f1)); + units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 250, + DateTickUnitType.MILLISECOND, 10, f1)); + units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 500, + DateTickUnitType.MILLISECOND, 50, f1)); + + // seconds + units.add(new DateTickUnit(DateTickUnitType.SECOND, 1, + DateTickUnitType.MILLISECOND, 50, f2)); + units.add(new DateTickUnit(DateTickUnitType.SECOND, 5, + DateTickUnitType.SECOND, 1, f2)); + units.add(new DateTickUnit(DateTickUnitType.SECOND, 10, + DateTickUnitType.SECOND, 1, f2)); + units.add(new DateTickUnit(DateTickUnitType.SECOND, 30, + DateTickUnitType.SECOND, 5, f2)); + + // minutes + units.add(new DateTickUnit(DateTickUnitType.MINUTE, 1, + DateTickUnitType.SECOND, 5, f3)); + units.add(new DateTickUnit(DateTickUnitType.MINUTE, 2, + DateTickUnitType.SECOND, 10, f3)); + units.add(new DateTickUnit(DateTickUnitType.MINUTE, 5, + DateTickUnitType.MINUTE, 1, f3)); + units.add(new DateTickUnit(DateTickUnitType.MINUTE, 10, + DateTickUnitType.MINUTE, 1, f3)); + units.add(new DateTickUnit(DateTickUnitType.MINUTE, 15, + DateTickUnitType.MINUTE, 5, f3)); + units.add(new DateTickUnit(DateTickUnitType.MINUTE, 20, + DateTickUnitType.MINUTE, 5, f3)); + units.add(new DateTickUnit(DateTickUnitType.MINUTE, 30, + DateTickUnitType.MINUTE, 5, f3)); + + // hours + units.add(new DateTickUnit(DateTickUnitType.HOUR, 1, + DateTickUnitType.MINUTE, 5, f3)); + units.add(new DateTickUnit(DateTickUnitType.HOUR, 2, + DateTickUnitType.MINUTE, 10, f3)); + units.add(new DateTickUnit(DateTickUnitType.HOUR, 4, + DateTickUnitType.MINUTE, 30, f3)); + units.add(new DateTickUnit(DateTickUnitType.HOUR, 6, + DateTickUnitType.HOUR, 1, f3)); + units.add(new DateTickUnit(DateTickUnitType.HOUR, 12, + DateTickUnitType.HOUR, 1, f4)); + + // days + units.add(new DateTickUnit(DateTickUnitType.DAY, 1, + DateTickUnitType.HOUR, 1, f5)); + units.add(new DateTickUnit(DateTickUnitType.DAY, 2, + DateTickUnitType.HOUR, 1, f5)); + units.add(new DateTickUnit(DateTickUnitType.DAY, 7, + DateTickUnitType.DAY, 1, f5)); + units.add(new DateTickUnit(DateTickUnitType.DAY, 15, + DateTickUnitType.DAY, 1, f5)); + + // months + units.add(new DateTickUnit(DateTickUnitType.MONTH, 1, + DateTickUnitType.DAY, 1, f6)); + units.add(new DateTickUnit(DateTickUnitType.MONTH, 2, + DateTickUnitType.DAY, 1, f6)); + units.add(new DateTickUnit(DateTickUnitType.MONTH, 3, + DateTickUnitType.MONTH, 1, f6)); + units.add(new DateTickUnit(DateTickUnitType.MONTH, 4, + DateTickUnitType.MONTH, 1, f6)); + units.add(new DateTickUnit(DateTickUnitType.MONTH, 6, + DateTickUnitType.MONTH, 1, f6)); + + // years + units.add(new DateTickUnit(DateTickUnitType.YEAR, 1, + DateTickUnitType.MONTH, 1, f7)); + units.add(new DateTickUnit(DateTickUnitType.YEAR, 2, + DateTickUnitType.MONTH, 3, f7)); + units.add(new DateTickUnit(DateTickUnitType.YEAR, 5, + DateTickUnitType.YEAR, 1, f7)); + units.add(new DateTickUnit(DateTickUnitType.YEAR, 10, + DateTickUnitType.YEAR, 1, f7)); + units.add(new DateTickUnit(DateTickUnitType.YEAR, 25, + DateTickUnitType.YEAR, 5, f7)); + units.add(new DateTickUnit(DateTickUnitType.YEAR, 50, + DateTickUnitType.YEAR, 10, f7)); + units.add(new DateTickUnit(DateTickUnitType.YEAR, 100, + DateTickUnitType.YEAR, 20, f7)); + + return units; + } + + + /** + * Method to get a message from resource bundle. + * + * @param locale Locale used to specify the resource bundle. + * @param key Key to specify the required message. + * @param def Default string if resource is not existing. + * + * @return Message + */ + protected String getMessage(Locale locale, String key, String def) { + return RessourceFactory.getInstance().getRessource(locale, key, def); + } + + + /** + * @see de.intevation.gnv.chart.AbstractXYLineChart#createSeriesName(String, + * String, String) + */ + protected String createSeriesName( + String breakPoint1, + String breakPoint2, + String breakPoint3 + ) { + log.debug("create seriesname of timeseries chart"); + return findValueTitle(parameters, breakPoint1) + + " " + + findValueTitle(measurements, breakPoint2) + + "m"; + } + + + /** + * Method to add gaps between two data points. The max valid space between + * two data points is calculated by <code>calculateGapSize</code>. + * + * @param results All data points in this dataset. + * @param series Series to be processed. + * @param startDate Date item where the scan for gaps should begin. + * @param endDate Date item where the scan should end. + * @param startPos Start position of this series in <code>results</code>. + * @param endPos End position of a series in <code>results</code> + */ + protected void addGaps( + Result[] results, + Series series, + Date startDate, + Date endDate, + int startPos, + int endPos + ) { + int gapID = results[startPos].getInteger("GAPID"); + long maxDiff = calculateGapSize( + startDate, endDate, startPos, endPos, gapID + ); + + if (log.isDebugEnabled()) { + log.debug("*****************************************************"); + log.debug("Values of gap detection."); + log.debug("Start date: " + startDate.toString()); + log.debug("End date: " + endDate.toString()); + long diff = endDate.getTime() - startDate.getTime(); + log.debug("Time difference (in ms): " + diff); + log.debug("Time difference (in h): " + (diff/(1000*60*60))); + log.debug("Configured gap size (in %): " + GAP_SIZE); + log.debug("Calculated gap size (in ms): " + maxDiff); + log.debug("Calculated gap size (in h): " + (maxDiff/(1000*60*60))); + log.debug("*****************************************************"); + } + + Date last = startDate; + for (int i = startPos+1; i < endPos; i++) { + Result res = results[i]; + Date now = res.getDate("XORDINATE"); + + if ((now.getTime() - last.getTime()) > maxDiff) { + // add gap, add 1 minute to last date and add null value + log.info( + "Gap between " + + last.toString() + " and " + now.toString() + ); + last.setTime(last.getTime() + 60000); + ((TimeSeries) series).addOrUpdate(new Minute(last), null); + } + + last = now; + } + } + + + /** + * Method to calculate the max space between two data points. + * + * @param start First date + * @param end Last date + * @param startPos Start position of the current series in the collection + * containing the bunch of series. + * @param endPos End position of the current series in the collection + * containing the bunch of series. + * @param gapID Gap id used to specify the time intervals. + * + * @return Min size of a gap. + */ + protected long calculateGapSize( + Date start, + Date end, + int startPos, + int endPos, + int gapID + ){ + long maxGap = (end.getTime() - start.getTime()) / 100 * GAP_SIZE; + long interval = getTimeGapValue(start, end, startPos, endPos, gapID); + + if (maxGap < interval) + maxGap = interval + 10; + + return maxGap; + } + + + /** + * Determine the interval size between two data points. + * + * @param dStart Start date + * @param dEnd End date + * @param pStart Index of start point in series used to specify the total + * amount of date items. + * @param pEnd Index of end point in series used to specify the total amount + * of date items. + * @param gapID Gap id used to determine gaps configured in a xml document. + * + * @return Interval size between two data points. + */ + protected long getTimeGapValue( + Date dStart, + Date dEnd, + int pStart, + int pEnd, + int gapID + ){ + long gap = 0; + + if (gapID < 0 || gapID >= 99) { + + if (gapID == -1) { + // no gaps in meshes + gap = NO_TIME_GAP; + } + else if (pEnd-pStart < 60) { + gap = (3/(pEnd-pStart)) * (dEnd.getTime() - dStart.getTime()); + } + } + else{ + Iterator it = timeGaps.iterator(); + + while (it.hasNext()) { + TimeGap tempTimeGap = (TimeGap) it.next(); + + if (tempTimeGap.getKey() == gapID){ + String unit = tempTimeGap.getUnit(); + int gapValue = tempTimeGap.getValue(); + + if (unit.equals(TimeGap.TIME_UNIT_MINUTE)) { + gap = gapValue * TimeGap.MINUTE_IN_MILLIS; + } + else if (unit.equals(TimeGap.TIME_UNIT_HOUR)) { + gap = gapValue * TimeGap.HOUR_IN_MILLIS; + } + else if (unit.equals(TimeGap.TIME_UNIT_DAY)) { + gap = gapValue * TimeGap.DAY_IN_MILLIS; + } + else if (unit.equals(TimeGap.TIME_UNIT_WEEK)) { + gap = gapValue * TimeGap.WEEK_IN_MILLIS; + } + else if (unit.equals(TimeGap.TIME_UNIT_MONTH)) { + gap = gapValue * (TimeGap.DAY_IN_MILLIS *30); + } + else if (unit.equals(TimeGap.TIME_UNIT_YEAR)) { + gap = gapValue * (TimeGap.DAY_IN_MILLIS *365); + } + break; + } + } + } + + return gap; + } +} +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :