comparison 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
comparison
equal deleted inserted replaced
1027:fca4b5eb8d2f 1119:7c4f81f74c47
1 /*
2 * Copyright (c) 2010 by Intevation GmbH
3 *
4 * This program is free software under the LGPL (>=v2.1)
5 * Read the file LGPL.txt coming with the software for details
6 * or visit http://www.gnu.org/licenses/ if it does not exist.
7 */
8
9 package de.intevation.gnv.chart;
10
11 import de.intevation.gnv.artifacts.ressource.RessourceFactory;
12
13 import de.intevation.gnv.geobackend.base.Result;
14
15 import de.intevation.gnv.state.describedata.KeyValueDescibeData;
16
17 import de.intevation.gnv.timeseries.gap.TimeGap;
18
19 import java.text.DateFormat;
20 import java.text.SimpleDateFormat;
21
22 import java.util.Collection;
23 import java.util.Date;
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.Locale;
27 import java.util.TimeZone;
28
29 import org.apache.log4j.Logger;
30
31 import org.jfree.chart.ChartFactory;
32 import org.jfree.chart.ChartTheme;
33
34 import org.jfree.chart.axis.Axis;
35 import org.jfree.chart.axis.DateAxis;
36 import org.jfree.chart.axis.DateTickUnit;
37 import org.jfree.chart.axis.DateTickUnitType;
38 import org.jfree.chart.axis.TickUnitSource;
39 import org.jfree.chart.axis.TickUnits;
40 import org.jfree.chart.axis.ValueAxis;
41
42 import org.jfree.chart.plot.PlotOrientation;
43 import org.jfree.chart.plot.XYPlot;
44
45 import org.jfree.data.general.Series;
46
47 import org.jfree.data.time.Minute;
48 import org.jfree.data.time.TimeSeries;
49 import org.jfree.data.time.TimeSeriesCollection;
50
51 /**
52 * This class is used to create timeseries charts. The domain axis contains
53 * multiple date/time objects.
54 *
55 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
56 */
57 public class TimeSeriesChart
58 extends AbstractXYLineChart
59 {
60
61 /**
62 * Constant format which can be useful to format date items. Value is
63 * {@value}.
64 */
65 public static final String DEFAULT_DATE_FORMAT = "dd-MMM-yyyy";
66
67 /**
68 * Constant field used if no gap detection should be done here. This field
69 * is used in @see #getTimeGapValue. Value is {@value}.
70 */
71 public static final long NO_TIME_GAP = Long.MAX_VALUE - 1000;
72
73 /**
74 * Percentage used for gap detection. Its value is {@value}.
75 */
76 public static int GAP_SIZE = 5; // in percent
77
78 /**
79 * Logger used for logging with log4j.
80 */
81 private static Logger log = Logger.getLogger(TimeSeriesChart.class);
82
83 static {
84 /* The percentage defining the width of a gap should be configured in
85 * conf.xml instead of being configured in a system property */
86 GAP_SIZE = Integer.getInteger("chart.gap.percentage", GAP_SIZE);
87 }
88
89
90 /**
91 * Constructor used to create <code>TimeSeries</code> charts.
92 *
93 * @param labels Labels used to be displayed in title, subtitle and so on.
94 * @param theme ChartTheme used to adjust the rendering of this chart.
95 * @param parameters Collection containing a bunch of parameters.
96 * @param measurements Collection containing a bunch of measurements.
97 * @param dates Collection containing a bunch of date objects.
98 * @param result Collection containing a bunch of <code>Result</code>
99 * objects which contain the actual data items to be displayed.
100 * @param timeGaps Collection with timegap definitions.
101 * @param locale Locale used to specify the format of labels, numbers, ...
102 * @param linesVisible Render lines between data points if true, otherwise
103 * not.
104 * @param shapesVisible Render vertices as points if true, otherwise not.
105 */
106 public TimeSeriesChart(
107 ChartLabels labels,
108 ChartTheme theme,
109 Collection parameters,
110 Collection measurements,
111 Collection dates,
112 Collection result,
113 Collection timeGaps,
114 Locale locale,
115 boolean linesVisible,
116 boolean shapesVisible
117 ) {
118 this.labels = labels;
119 this.theme = theme;
120 this.parameters = parameters;
121 this.measurements = measurements;
122 this.dates = dates;
123 this.resultSet = result;
124 this.timeGaps = timeGaps;
125 this.locale = locale;
126 this.PLOT_ORIENTATION = PlotOrientation.VERTICAL;
127 this.linesVisible = linesVisible;
128 this.shapesVisible = shapesVisible;
129 this.datasets = new HashMap();
130 this.ranges = new HashMap();
131 }
132
133
134 /**
135 * see de.intevation.gnv.chart.AbstractXYLineChart#initChart()
136 */
137 @Override
138 protected void initChart() {
139 chart = ChartFactory.createTimeSeriesChart(
140 labels.getTitle(),
141 labels.getDomainAxisLabel(),
142 null,
143 null,
144 true,
145 false,
146 false
147 );
148
149 XYPlot plot = (XYPlot) chart.getPlot();
150 plot.setDomainAxis(0, new DateAxis(
151 labels.getDomainAxisLabel(), TimeZone.getDefault(), locale));
152 }
153
154
155 /**
156 * @see de.intevation.gnv.chart.AbstractXYLineChart#initData()
157 */
158 protected void initData() {
159 log.debug("init data for timeseries chart");
160
161 String breakPoint1 = null;
162 String breakPoint2 = null;
163 String breakPoint3 = null;
164
165 Iterator iter = resultSet.iterator();
166 Result row = null;
167 String seriesName = null;
168 String parameter = null;
169 TimeSeries series = null;
170
171 int idx = 0;
172 int startPos = 0;
173 int endPos = 0;
174 Date startDate = null;
175 Date endDate = null;
176
177 Result[] results =
178 (Result[]) resultSet.toArray(new Result[resultSet.size()]);
179
180 while (iter.hasNext()) {
181 row = (Result) iter.next();
182
183 // add current data to plot and prepare for next one
184 if (!row.getString("GROUP1").equals(breakPoint1) ||
185 !row.getString("GROUP2").equals(breakPoint2) ||
186 !row.getString("GROUP3").equals(breakPoint3)
187 ) {
188 log.debug("prepare data/plot for next dataset");
189
190 if(series != null) {
191 // add gaps before adding series to chart
192 startDate = results[startPos].getDate("XORDINATE");
193 endDate = results[endPos-1].getDate("XORDINATE");
194 addGaps(results,series,startDate,endDate,startPos,endPos);
195 addSeries(series, parameter, idx);
196
197 startPos = endPos + 1;
198 }
199
200 // prepare variables for next plot
201 breakPoint1 = row.getString("GROUP1");
202 breakPoint2 = row.getString("GROUP2");
203 breakPoint3 = row.getString("GROUP3");
204
205 seriesName = createSeriesName(
206 breakPoint1,
207 breakPoint2,
208 breakPoint3
209 );
210 parameter = findParameter(seriesName);
211
212 log.debug("next dataset is '" + seriesName + "'");
213 series = new TimeSeries(seriesName, Minute.class);
214 }
215
216 addValue(row, series);
217 storeMaxRange(ranges, row.getDouble("YORDINATE"), parameter);
218 endPos++;
219 }
220
221 if (startPos < results.length && endPos-1 < results.length) {
222 // add the last dataset if existing to plot and prepare its axis
223 startDate = results[startPos].getDate("XORDINATE");
224 endDate = results[endPos-1].getDate("XORDINATE");
225 addGaps(results, series, startDate, endDate, startPos, endPos);
226 addSeries(series, parameter, idx);
227 }
228
229 addDatasets();
230 }
231
232
233 /**
234 * @see de.intevation.gnv.chart.AbstractXYLineChart#addValue(Result, Series)
235 */
236 protected void addValue(Result row, Series series) {
237 ((TimeSeries) series).addOrUpdate(
238 new Minute(row.getDate("XORDINATE")),
239 row.getDouble("YORDINATE")
240 );
241 }
242
243
244 /**
245 * @param parameter
246 * @see de.intevation.gnv.chart.AbstractXYLineChart#addSeries(Series,
247 * String, int)
248 */
249 protected void addSeries(Series series, String parameter, int idx) {
250 log.debug("add series (" + parameter + ")to timeseries chart");
251
252 if (series == null) {
253 log.warn("no data to add");
254 return;
255 }
256
257 TimeSeriesCollection tsc = null;
258
259 if (datasets.containsKey(parameter))
260 tsc = (TimeSeriesCollection) datasets.get(parameter);
261 else
262 tsc = new TimeSeriesCollection();
263
264 tsc.addSeries((TimeSeries) series);
265 datasets.put(parameter, tsc);
266 }
267
268
269 /**
270 * Method to add processed datasets to plot. Each dataset is adjusted using
271 * <code>prepareAxis</code> and <code>adjustRenderer</code> methods.
272 */
273 protected void addDatasets() {
274 Iterator iter = parameters.iterator();
275 XYPlot plot = chart.getXYPlot();
276 int idx = 0;
277
278 TimeSeriesCollection tsc = null;
279 KeyValueDescibeData data = null;
280 String key = null;
281 while (iter.hasNext()) {
282 data = (KeyValueDescibeData) iter.next();
283 key = data.getValue();
284
285 if (datasets.containsKey(key)) {
286 tsc = (TimeSeriesCollection)datasets.get(key);
287 plot.setDataset(idx, tsc );
288 log.debug("Added " + key + " parameter to plot.");
289 prepareAxis(key, idx);
290 adjustRenderer(
291 idx++,
292 tsc.getSeriesCount(),
293 linesVisible,
294 shapesVisible
295 );
296 }
297 }
298 }
299
300
301 /**
302 * @param locale
303 * @see de.intevation.gnv.chart.AbstractXYLineChart#localizeDomainAxis(Axis,
304 * Locale)
305 */
306 protected void localizeDomainAxis(Axis axis, Locale locale) {
307 ((ValueAxis)axis).setStandardTickUnits(createStandardDateTickUnits(
308 TimeZone.getDefault(),
309 locale));
310 }
311
312
313 /**
314 * @param zone
315 * @param locale
316 * @return TickUnitSource
317 * @see org.jfree.chart.axis.DateAxis#createStandardDateTickUnits(TimeZone,
318 * Locale)
319 */
320 public static TickUnitSource createStandardDateTickUnits(
321 TimeZone zone,
322 Locale locale)
323 {
324 /*
325 * This method have been copied from JFreeChart's DateAxis class.
326 * DateFormat objects are hard coded in DateAxis and cannot be adjusted.
327 */
328 if (zone == null) {
329 throw new IllegalArgumentException("Null 'zone' argument.");
330 }
331 if (locale == null) {
332 throw new IllegalArgumentException("Null 'locale' argument.");
333 }
334 TickUnits units = new TickUnits();
335
336 // date formatters
337 DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS", locale);
338 DateFormat f2 = new SimpleDateFormat("HH:mm:ss", locale);
339 DateFormat f3 = new SimpleDateFormat("HH:mm", locale);
340 DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm", locale);
341 DateFormat f5 = new SimpleDateFormat("d-MMM yyyy", locale);
342 DateFormat f6 = new SimpleDateFormat("MMM-yyyy", locale);
343 DateFormat f7 = new SimpleDateFormat("yyyy", locale);
344
345 f1.setTimeZone(zone);
346 f2.setTimeZone(zone);
347 f3.setTimeZone(zone);
348 f4.setTimeZone(zone);
349 f5.setTimeZone(zone);
350 f6.setTimeZone(zone);
351 f7.setTimeZone(zone);
352
353 // milliseconds
354 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 1, f1));
355 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 5,
356 DateTickUnitType.MILLISECOND, 1, f1));
357 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 10,
358 DateTickUnitType.MILLISECOND, 1, f1));
359 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 25,
360 DateTickUnitType.MILLISECOND, 5, f1));
361 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 50,
362 DateTickUnitType.MILLISECOND, 10, f1));
363 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 100,
364 DateTickUnitType.MILLISECOND, 10, f1));
365 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 250,
366 DateTickUnitType.MILLISECOND, 10, f1));
367 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 500,
368 DateTickUnitType.MILLISECOND, 50, f1));
369
370 // seconds
371 units.add(new DateTickUnit(DateTickUnitType.SECOND, 1,
372 DateTickUnitType.MILLISECOND, 50, f2));
373 units.add(new DateTickUnit(DateTickUnitType.SECOND, 5,
374 DateTickUnitType.SECOND, 1, f2));
375 units.add(new DateTickUnit(DateTickUnitType.SECOND, 10,
376 DateTickUnitType.SECOND, 1, f2));
377 units.add(new DateTickUnit(DateTickUnitType.SECOND, 30,
378 DateTickUnitType.SECOND, 5, f2));
379
380 // minutes
381 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 1,
382 DateTickUnitType.SECOND, 5, f3));
383 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 2,
384 DateTickUnitType.SECOND, 10, f3));
385 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 5,
386 DateTickUnitType.MINUTE, 1, f3));
387 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 10,
388 DateTickUnitType.MINUTE, 1, f3));
389 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 15,
390 DateTickUnitType.MINUTE, 5, f3));
391 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 20,
392 DateTickUnitType.MINUTE, 5, f3));
393 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 30,
394 DateTickUnitType.MINUTE, 5, f3));
395
396 // hours
397 units.add(new DateTickUnit(DateTickUnitType.HOUR, 1,
398 DateTickUnitType.MINUTE, 5, f3));
399 units.add(new DateTickUnit(DateTickUnitType.HOUR, 2,
400 DateTickUnitType.MINUTE, 10, f3));
401 units.add(new DateTickUnit(DateTickUnitType.HOUR, 4,
402 DateTickUnitType.MINUTE, 30, f3));
403 units.add(new DateTickUnit(DateTickUnitType.HOUR, 6,
404 DateTickUnitType.HOUR, 1, f3));
405 units.add(new DateTickUnit(DateTickUnitType.HOUR, 12,
406 DateTickUnitType.HOUR, 1, f4));
407
408 // days
409 units.add(new DateTickUnit(DateTickUnitType.DAY, 1,
410 DateTickUnitType.HOUR, 1, f5));
411 units.add(new DateTickUnit(DateTickUnitType.DAY, 2,
412 DateTickUnitType.HOUR, 1, f5));
413 units.add(new DateTickUnit(DateTickUnitType.DAY, 7,
414 DateTickUnitType.DAY, 1, f5));
415 units.add(new DateTickUnit(DateTickUnitType.DAY, 15,
416 DateTickUnitType.DAY, 1, f5));
417
418 // months
419 units.add(new DateTickUnit(DateTickUnitType.MONTH, 1,
420 DateTickUnitType.DAY, 1, f6));
421 units.add(new DateTickUnit(DateTickUnitType.MONTH, 2,
422 DateTickUnitType.DAY, 1, f6));
423 units.add(new DateTickUnit(DateTickUnitType.MONTH, 3,
424 DateTickUnitType.MONTH, 1, f6));
425 units.add(new DateTickUnit(DateTickUnitType.MONTH, 4,
426 DateTickUnitType.MONTH, 1, f6));
427 units.add(new DateTickUnit(DateTickUnitType.MONTH, 6,
428 DateTickUnitType.MONTH, 1, f6));
429
430 // years
431 units.add(new DateTickUnit(DateTickUnitType.YEAR, 1,
432 DateTickUnitType.MONTH, 1, f7));
433 units.add(new DateTickUnit(DateTickUnitType.YEAR, 2,
434 DateTickUnitType.MONTH, 3, f7));
435 units.add(new DateTickUnit(DateTickUnitType.YEAR, 5,
436 DateTickUnitType.YEAR, 1, f7));
437 units.add(new DateTickUnit(DateTickUnitType.YEAR, 10,
438 DateTickUnitType.YEAR, 1, f7));
439 units.add(new DateTickUnit(DateTickUnitType.YEAR, 25,
440 DateTickUnitType.YEAR, 5, f7));
441 units.add(new DateTickUnit(DateTickUnitType.YEAR, 50,
442 DateTickUnitType.YEAR, 10, f7));
443 units.add(new DateTickUnit(DateTickUnitType.YEAR, 100,
444 DateTickUnitType.YEAR, 20, f7));
445
446 return units;
447 }
448
449
450 /**
451 * Method to get a message from resource bundle.
452 *
453 * @param locale Locale used to specify the resource bundle.
454 * @param key Key to specify the required message.
455 * @param def Default string if resource is not existing.
456 *
457 * @return Message
458 */
459 protected String getMessage(Locale locale, String key, String def) {
460 return RessourceFactory.getInstance().getRessource(locale, key, def);
461 }
462
463
464 /**
465 * @see de.intevation.gnv.chart.AbstractXYLineChart#createSeriesName(String,
466 * String, String)
467 */
468 protected String createSeriesName(
469 String breakPoint1,
470 String breakPoint2,
471 String breakPoint3
472 ) {
473 log.debug("create seriesname of timeseries chart");
474 return findValueTitle(parameters, breakPoint1) +
475 " " +
476 findValueTitle(measurements, breakPoint2) +
477 "m";
478 }
479
480
481 /**
482 * Method to add gaps between two data points. The max valid space between
483 * two data points is calculated by <code>calculateGapSize</code>.
484 *
485 * @param results All data points in this dataset.
486 * @param series Series to be processed.
487 * @param startDate Date item where the scan for gaps should begin.
488 * @param endDate Date item where the scan should end.
489 * @param startPos Start position of this series in <code>results</code>.
490 * @param endPos End position of a series in <code>results</code>
491 */
492 protected void addGaps(
493 Result[] results,
494 Series series,
495 Date startDate,
496 Date endDate,
497 int startPos,
498 int endPos
499 ) {
500 int gapID = results[startPos].getInteger("GAPID");
501 long maxDiff = calculateGapSize(
502 startDate, endDate, startPos, endPos, gapID
503 );
504
505 if (log.isDebugEnabled()) {
506 log.debug("*****************************************************");
507 log.debug("Values of gap detection.");
508 log.debug("Start date: " + startDate.toString());
509 log.debug("End date: " + endDate.toString());
510 long diff = endDate.getTime() - startDate.getTime();
511 log.debug("Time difference (in ms): " + diff);
512 log.debug("Time difference (in h): " + (diff/(1000*60*60)));
513 log.debug("Configured gap size (in %): " + GAP_SIZE);
514 log.debug("Calculated gap size (in ms): " + maxDiff);
515 log.debug("Calculated gap size (in h): " + (maxDiff/(1000*60*60)));
516 log.debug("*****************************************************");
517 }
518
519 Date last = startDate;
520 for (int i = startPos+1; i < endPos; i++) {
521 Result res = results[i];
522 Date now = res.getDate("XORDINATE");
523
524 if ((now.getTime() - last.getTime()) > maxDiff) {
525 // add gap, add 1 minute to last date and add null value
526 log.info(
527 "Gap between " +
528 last.toString() + " and " + now.toString()
529 );
530 last.setTime(last.getTime() + 60000);
531 ((TimeSeries) series).addOrUpdate(new Minute(last), null);
532 }
533
534 last = now;
535 }
536 }
537
538
539 /**
540 * Method to calculate the max space between two data points.
541 *
542 * @param start First date
543 * @param end Last date
544 * @param startPos Start position of the current series in the collection
545 * containing the bunch of series.
546 * @param endPos End position of the current series in the collection
547 * containing the bunch of series.
548 * @param gapID Gap id used to specify the time intervals.
549 *
550 * @return Min size of a gap.
551 */
552 protected long calculateGapSize(
553 Date start,
554 Date end,
555 int startPos,
556 int endPos,
557 int gapID
558 ){
559 long maxGap = (end.getTime() - start.getTime()) / 100 * GAP_SIZE;
560 long interval = getTimeGapValue(start, end, startPos, endPos, gapID);
561
562 if (maxGap < interval)
563 maxGap = interval + 10;
564
565 return maxGap;
566 }
567
568
569 /**
570 * Determine the interval size between two data points.
571 *
572 * @param dStart Start date
573 * @param dEnd End date
574 * @param pStart Index of start point in series used to specify the total
575 * amount of date items.
576 * @param pEnd Index of end point in series used to specify the total amount
577 * of date items.
578 * @param gapID Gap id used to determine gaps configured in a xml document.
579 *
580 * @return Interval size between two data points.
581 */
582 protected long getTimeGapValue(
583 Date dStart,
584 Date dEnd,
585 int pStart,
586 int pEnd,
587 int gapID
588 ){
589 long gap = 0;
590
591 if (gapID < 0 || gapID >= 99) {
592
593 if (gapID == -1) {
594 // no gaps in meshes
595 gap = NO_TIME_GAP;
596 }
597 else if (pEnd-pStart < 60) {
598 gap = (3/(pEnd-pStart)) * (dEnd.getTime() - dStart.getTime());
599 }
600 }
601 else{
602 Iterator it = timeGaps.iterator();
603
604 while (it.hasNext()) {
605 TimeGap tempTimeGap = (TimeGap) it.next();
606
607 if (tempTimeGap.getKey() == gapID){
608 String unit = tempTimeGap.getUnit();
609 int gapValue = tempTimeGap.getValue();
610
611 if (unit.equals(TimeGap.TIME_UNIT_MINUTE)) {
612 gap = gapValue * TimeGap.MINUTE_IN_MILLIS;
613 }
614 else if (unit.equals(TimeGap.TIME_UNIT_HOUR)) {
615 gap = gapValue * TimeGap.HOUR_IN_MILLIS;
616 }
617 else if (unit.equals(TimeGap.TIME_UNIT_DAY)) {
618 gap = gapValue * TimeGap.DAY_IN_MILLIS;
619 }
620 else if (unit.equals(TimeGap.TIME_UNIT_WEEK)) {
621 gap = gapValue * TimeGap.WEEK_IN_MILLIS;
622 }
623 else if (unit.equals(TimeGap.TIME_UNIT_MONTH)) {
624 gap = gapValue * (TimeGap.DAY_IN_MILLIS *30);
625 }
626 else if (unit.equals(TimeGap.TIME_UNIT_YEAR)) {
627 gap = gapValue * (TimeGap.DAY_IN_MILLIS *365);
628 }
629 break;
630 }
631 }
632 }
633
634 return gap;
635 }
636 }
637 // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :

http://dive4elements.wald.intevation.org