comparison gnv-artifacts/src/main/java/de/intevation/gnv/chart/TimeSeriesChart.java @ 875:5e9efdda6894

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

http://dive4elements.wald.intevation.org