Mercurial > dive4elements > gnv-client
view gnv-artifacts/src/main/java/de/intevation/gnv/chart/TimeSeriesChart.java @ 1129:ccfa07b88476
merged geo-backend
author | Thomas Arendsen Hein <thomas@intevation.de> |
---|---|
date | Fri, 28 Sep 2012 12:14:01 +0200 |
parents | f953c9a559d8 |
children |
line wrap: on
line source
/* * 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 :