comparison gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalProfileChart.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.geobackend.base.Result;
12
13 import de.intevation.gnv.state.describedata.KeyValueDescibeData;
14
15 import java.util.Collection;
16 import java.util.HashMap;
17 import java.util.Iterator;
18 import java.util.Locale;
19 import java.util.Map;
20
21 import org.apache.log4j.Logger;
22
23 import org.jfree.chart.ChartTheme;
24
25 import org.jfree.chart.axis.Axis;
26 import org.jfree.chart.axis.NumberAxis;
27
28 import org.jfree.chart.plot.PlotOrientation;
29 import org.jfree.chart.plot.XYPlot;
30
31 import org.jfree.data.Range;
32
33 import org.jfree.data.general.Series;
34
35 import org.jfree.data.xy.XYSeries;
36 import org.jfree.data.xy.XYSeriesCollection;
37
38 /**
39 * This class is used to create xy charts of vertical profiles.
40 *
41 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
42 */
43 public class VerticalProfileChart
44 extends AbstractXYLineChart
45 {
46 /**
47 * Default axis identifier which is used if @see #getDependendAxisName does
48 * not return a value. The value of this field is {@value}.
49 */
50 public static final String DEFAULT_AXIS = "KPOSITION";
51
52 /**
53 * Logger used for logging with log4j.
54 */
55 private static Logger log = Logger.getLogger(VerticalProfileChart.class);
56
57 /**
58 * Constant used for gap detection. Its value is {@value}.
59 */
60 protected static int PERCENTAGE = 5;
61
62 /**
63 * Constnat used for gap detection in @see #gridDetection.
64 */
65 protected final double GAP_MAX_LEVEL = Math.sqrt(2.0);
66
67 /**
68 * Constant used for gap detection in @see #addGaps. Its value is {@value}.
69 */
70 protected final int GAP_MAX_VALUES = 60;
71
72 /**
73 * Map to store max ranges of each parameter
74 * (org.jfree.chart.axis.Axis.setAutoRange(true) doesn't seem to work
75 * properly.
76 */
77 protected Map values;
78
79 static {
80 /* The percentage defining the width of a gap should be configured in
81 * conf.xml instead of being configured in a system property */
82 PERCENTAGE = Integer.getInteger("chart.gap.percentage", PERCENTAGE);
83 }
84
85
86 /**
87 * Constructor used to create xy-charts.
88 *
89 * @param labels Labels used to be displayed in title, subtitle and so on.
90 * @param theme ChartTheme used to adjust the rendering of this chart.
91 * @param parameters Collection containing a bunch of parameters.
92 * @param measurements Collection containing a bunch of measurements.
93 * @param dates Collection containing a bunch of date objects.
94 * @param result Collection containing a bunch of <code>Result</code>
95 * objects which contain the actual data items to be displayed.
96 * @param timeGaps Collection with timegap definitions.
97 * @param locale Locale used to specify the format of labels, numbers, ...
98 * @param linesVisible Render lines between data points if true, otherwise
99 * not.
100 * @param shapesVisible Render vertices as points if true, otherwise not.
101 */
102 public VerticalProfileChart(
103 ChartLabels labels,
104 ChartTheme theme,
105 Collection parameters,
106 Collection measurements,
107 Collection dates,
108 Collection result,
109 Collection timeGaps,
110 Locale locale,
111 boolean linesVisible,
112 boolean shapesVisible
113 ) {
114 this.labels = labels;
115 this.theme = theme;
116 this.parameters = parameters;
117 this.measurements = measurements;
118 this.dates = dates;
119 this.resultSet = result;
120 this.timeGaps = timeGaps;
121 this.locale = locale;
122 this.PLOT_ORIENTATION = PlotOrientation.HORIZONTAL;
123 this.linesVisible = linesVisible;
124 this.shapesVisible = shapesVisible;
125 this.datasets = new HashMap();
126 this.ranges = new HashMap();
127 this.values = new HashMap();
128 }
129
130
131 /**
132 * @see de.intevation.gnv.chart.AbstractXYLineChart#initData()
133 */
134 @Override
135 protected void initData() {
136 log.debug("init data for VerticalProfileChart");
137
138 int items = resultSet.size();
139 log.debug("Found " + items + " items for this chart.");
140
141 String breakPoint1 = null;
142 String breakPoint2 = null;
143 String breakPoint3 = null;
144
145 Iterator iter = resultSet.iterator();
146 Result row = null;
147 String seriesName = null;
148 String parameter = null;
149 XYSeries series = null;
150
151 int idx = 0;
152 int startPos = 0;
153 int endPos = 0;
154 double startValue = 0;
155 double endValue = 0;
156
157 Result[] results =
158 (Result[]) resultSet.toArray(new Result[resultSet.size()]);
159
160 while (iter.hasNext()) {
161 row = (Result) iter.next();
162
163 // add current data to plot and prepare for next one
164 if (!row.getString("GROUP1").equals(breakPoint1) ||
165 !row.getString("GROUP2").equals(breakPoint2) ||
166 !row.getString("GROUP3").equals(breakPoint3)
167 ) {
168 log.debug("prepare data/plot for next dataset");
169
170 if(series != null) {
171 if (startPos >= 0 && endPos < items) {
172 gapDetection(results, series, startPos, endPos);
173 }
174 addSeries(series, parameter, idx);
175
176 startPos = endPos +1;
177 }
178
179 // prepare variables for next plot
180 breakPoint1 = row.getString("GROUP1");
181 breakPoint2 = row.getString("GROUP2");
182 breakPoint3 = row.getString("GROUP3");
183
184 seriesName = createSeriesName(
185 breakPoint1,
186 breakPoint2,
187 breakPoint3
188 );
189 parameter = findParameter(seriesName);
190
191 log.debug("next dataset is '" + seriesName + "'");
192 series = new XYSeries(seriesName);
193 }
194
195 addValue(row, series);
196 Object x = getValue(row);
197 Double y = row.getDouble("YORDINATE");
198 if (x != null && y != null) {
199 storeMaxRange(ranges, y, parameter);
200 storeMaxValue(values, x, parameter);
201 }
202 endPos++;
203 }
204
205 if (items == 0)
206 return;
207
208 if (startPos >= 0 && endPos < items) {
209 gapDetection(results, series, startPos, endPos);
210 }
211 addSeries(series, parameter, idx);
212
213 addDatasets();
214 }
215
216
217 /**
218 * Extract the important value from <code>Result</code> object.
219 *
220 * @param row <code>Result</code> object which contains a required value.
221 *
222 * @return X-ordinate
223 */
224 protected Object getValue(Result row) {
225 return row.getDouble("XORDINATE");
226 }
227
228
229 /**
230 * General method to start a gap detection. The switch between standard gap
231 * detection method <code>addGaps</code> and a specialized method
232 * <code>addGapsOnGrid</code> is done by a parameter <code>DATEID</code>
233 * which is stored a each <code>Result</code> object. Specialized method is
234 * used if <code>DATEID</code> equals 2, otherwise the standard method is
235 * used.
236 *
237 * @param results Array of <code>Result</code> objects storing data of
238 * this chart.
239 * @param series Series used to add gaps.
240 * @param startPos Index of first element of series in results.
241 * @param endPos Index of last element of series in results.
242 */
243 protected void gapDetection(
244 Result[] results,
245 Series series,
246 int startPos,
247 int endPos
248 ) {
249 double startValue = results[startPos].getDouble("XORDINATE");
250 double endValue = results[endPos-1].getDouble("XORDINATE");
251 if (results[0].getInteger("DATAID") == 2)
252 addGapsOnGrid(results, series, startPos, endPos);
253 else
254 addGaps(results, series, startValue, endValue, startPos, endPos);
255 }
256
257 @Override
258 protected void prepareAxis(String seriesKey, int idx) {
259 super.prepareAxis(seriesKey, idx);
260
261 XYPlot plot = chart.getXYPlot();
262 NumberAxis domainAxis = (NumberAxis) plot.getRangeAxis(idx);
263
264 Range domainRange = domainAxis.getRange();
265 log.debug("Domain axis range before: " + domainRange.toString());
266
267 domainRange = Range.expand(domainRange, LOWER_MARGIN, UPPER_MARGIN);
268
269 double lower = domainRange.getLowerBound();
270 double upper = domainRange.getUpperBound();
271
272 if (lower == upper) {
273 double lo = lower > 0 ? lower - lower*0.05d : lower + lower*0.05d;
274 double up = upper > 0 ? upper + upper*0.05d : upper - upper*0.05d;
275
276 domainRange = new Range(lo, up);
277 }
278
279 log.debug("Range axis range after: " + domainRange.toString());
280 domainAxis.setRange(domainRange);
281 plot.setRangeAxis(idx, domainAxis);
282 }
283
284
285 /**
286 * Method to expand max range of a range axis.
287 * <code>LOWER_MARGIN</code> and <code>UPPER_MARGIN</code> are used to
288 * expand the range.
289 */
290 protected void prepareRangeAxis(String seriesKey, int idx) {
291 log.debug("Adjust domain range now...");
292 XYPlot plot = chart.getXYPlot();
293 NumberAxis yAxis = (NumberAxis) plot.getDomainAxis();
294
295 Range yRange = yAxis.getRange();
296 double lo = yRange.getLowerBound();
297 double hi = yRange.getUpperBound();
298
299 Iterator iter = values.values().iterator();
300 while (iter.hasNext()) {
301 Range tmp = (Range) iter.next();
302 log.debug("Series range: " + tmp.toString());
303
304 lo = lo < tmp.getLowerBound() ? lo : tmp.getLowerBound();
305 hi = hi > tmp.getUpperBound() ? hi : tmp.getUpperBound();
306 }
307
308 Range merged = Range.expand(
309 new Range(lo, hi),
310 LOWER_MARGIN, UPPER_MARGIN);
311 log.debug("Calculated range for all series = " + merged.toString());
312
313 yAxis.setRange(merged);
314 plot.setDomainAxis(yAxis);
315 }
316
317
318 /**
319 * @see de.intevation.gnv.chart.AbstractXYLineChart#addValue(Result, Series)
320 */
321 @Override
322 protected void addValue(Result row, Series series) {
323 ((XYSeries) series).add(
324 row.getDouble("XORDINATE"),
325 row.getDouble("YORDINATE")
326 );
327 }
328
329
330 /**
331 * @param parameter
332 * @see de.intevation.gnv.chart.AbstractXYLineChart#addSeries(Series, String,
333 * int)
334 */
335 @Override
336 protected void addSeries(Series series, String parameter, int idx) {
337 log.debug("add series (" + parameter + ")to chart");
338
339 if (series == null) {
340 log.warn("no data to add");
341 return;
342 }
343
344 XYSeriesCollection xysc = null;
345
346 if (datasets.containsKey(parameter))
347 xysc = (XYSeriesCollection) datasets.get(parameter);
348 else
349 xysc = new XYSeriesCollection();
350
351 xysc.addSeries((XYSeries) series);
352 datasets.put(parameter, xysc);
353 }
354
355
356 /**
357 * Method to add processed datasets to plot. Each dataset is adjusted using
358 * <code>prepareAxis</code> and <code>adjustRenderer</code> methods.
359 */
360 protected void addDatasets() {
361 Iterator iter = parameters.iterator();
362 XYPlot plot = chart.getXYPlot();
363 int idx = 0;
364
365 XYSeriesCollection xysc = null;
366 KeyValueDescibeData data = null;
367 String key = null;
368 while (iter.hasNext()) {
369 data = (KeyValueDescibeData) iter.next();
370 key = data.getValue();
371
372 if (datasets.containsKey(key)) {
373 xysc = (XYSeriesCollection)datasets.get(key);
374 plot.setDataset(idx, xysc );
375 log.debug("Added " + key + " parameter to plot.");
376 prepareAxis(key, idx);
377 adjustRenderer(
378 idx++,
379 xysc.getSeriesCount(),
380 linesVisible,
381 shapesVisible
382 );
383 }
384 }
385
386 prepareRangeAxis(null, -1);
387 }
388
389
390 /**
391 * Method used to store the max y-range of each parameter in this chart.
392 *
393 * @param values Map to store max values for each parameter.
394 * @param val Value used to be a Double.
395 * @param parameter Title used to identify a range object stored in values.
396 */
397 protected void storeMaxValue(Map values, Object val, String parameter) {
398 double value = ((Double) val).doubleValue();
399 Range range = null;
400
401 range = values.containsKey(parameter)
402 ? (Range) values.get(parameter)
403 : new Range(value, value);
404
405 double lower = range.getLowerBound();
406 double upper = range.getUpperBound();
407
408 lower = value < lower ? value : lower;
409 upper = value > upper ? value : upper;
410
411 values.put(parameter, new Range(lower, upper));
412 }
413
414
415 /**
416 * @param locale
417 * @see de.intevation.gnv.chart.AbstractXYLineChart#localizeDomainAxis(Axis,
418 * Locale)
419 */
420 @Override
421 protected void localizeDomainAxis(Axis axis, Locale locale) {
422 // call localizeRangeAxis from superclass which formats NumberAxis
423 super.localizeRangeAxis(axis, locale);
424 }
425
426
427 /**
428 * @see de.intevation.gnv.chart.AbstractXYLineChart#createSeriesName(String,
429 * String, String)
430 */
431 @Override
432 protected String createSeriesName(
433 String breakPoint1,
434 String breakPoint2,
435 String breakPoint3
436 ) {
437 log.debug("create seriesname of verticalprofile chart");
438 return findValueTitle(parameters, breakPoint1) +
439 " " +
440 findValueTitle(measurements, breakPoint2) +
441 "m";
442 }
443
444
445 /**
446 * Method used to add gaps between data points on grids. The real detection
447 * is done in <code>gridDetection</code>.
448 *
449 * @param results Array of <code>Result</code> objects storing the relevant
450 * values.
451 * @param series Series object where the gaps are added to.
452 * @param startPos Index of first element which should be used in gap
453 * detection. Other series are stored in results as well.
454 * @param endPos Index of last element which should be used in gap
455 * detection.
456 */
457 protected void addGapsOnGrid(
458 Result[] results,
459 Series series,
460 int startPos,
461 int endPos
462 ) {
463 String axis = null;
464
465 if (results.length > (startPos+1)) {
466 axis = getDependendAxisName(
467 results[startPos],
468 results[startPos+1]
469 );
470 }
471 else {
472 axis = DEFAULT_AXIS;
473 }
474
475 double range = 0;
476 int last = 0;
477 int current = 0;
478
479 for (int i = startPos+1; i < endPos; i++) {
480 last = results[i-1].getInteger(axis);
481 current = results[i].getInteger(axis);
482
483 boolean detected = gridDetection(last, current);
484
485 if (detected) {
486 double xOld = results[i-1].getDouble("XORDINATE");
487 double xNow = results[i].getDouble("XORDINATE");
488 log.debug("Gap detected on grid between "+ xOld +" and "+ xNow);
489 ((XYSeries) series).add(xOld+0.0001, null);
490 }
491 }
492 }
493
494
495 /**
496 * Standarad method to add gaps. There are two different methods to detect
497 * gaps. <code>simpleDetection</code> is used if the number of data points
498 * in this chart is lower than <code>GAP_MAX_VALUES</code>. Otherwise
499 * <code>specialDetection</code> is used. A data point with
500 * <code>null</code> value is added where a gap should be. This lets
501 * JFreeChart break the current graph.
502 *
503 * @param results Array of <code>Result</code> objects storing the relevant
504 * values.
505 * @param series Series object where the gaps are added to.
506 * @param startValue First data point value in series.
507 * @param endValue Last data point value in series.
508 * @param startPos Index of first data point in results which contains all
509 * data points of all series.
510 * @param endPos Index of last data point in results which contains all data
511 * points of all series.
512 */
513 protected void addGaps(
514 Result[] results,
515 Series series,
516 double startValue,
517 double endValue,
518 int startPos,
519 int endPos
520 ) {
521
522 double last = 0;
523 double current = 0;
524 int num = results.length;
525
526 for (int i = startPos+1; i < endPos; i++) {
527 boolean detected = false;
528
529 last = results[i-1].getDouble("YORDINATE");
530 current = results[i].getDouble("YORDINATE");
531
532 // gap detection for more than GAP_MAX_VALUES values
533 if (num > GAP_MAX_VALUES)
534 detected = simpleDetection(startValue, endValue, last, current);
535 // gap detection for less than GAP_MAX_VALUES values
536 else
537 detected = specialDetection(
538 startValue,
539 endValue,
540 last,
541 current,
542 num
543 );
544
545 if (detected) {
546 log.info("Gap between " + last + " and " + current);
547 ((XYSeries) series).add((last+current)/2, null);
548 }
549 }
550 }
551
552
553 /**
554 * Simple method to detect gaps. A gap is detected if the delta between two
555 * data points (current, last) is bigger than <code>PERCENTAGE</code> percent
556 * of delta of start and end.
557 * <br>
558 * (smallDelta &gt; delta / 100 * PERCENTAGE)
559 *
560 * @param start First data point value in a series.
561 * @param end Last data point value in a series.
562 * @param last Left value
563 * @param current Right value
564 *
565 * @return true, if a gap is detected between last and current - otherwise
566 * false.
567 */
568 protected boolean simpleDetection(
569 double start,
570 double end,
571 double last,
572 double current
573 ) {
574 double delta = Math.abs(end - start);
575 double smallDelta = Math.abs(current - last);
576
577 return (smallDelta > delta / 100 * PERCENTAGE);
578 }
579
580
581 /**
582 * Method to detect gaps between two data points. Following formula is used
583 * for detection:<br>
584 * smallDelta &gt; (3.0 / (count - 1) * delta)<br>
585 * smallDelta = current - last<br>
586 * delta = end - start
587 *
588 * @param start First data point value in a series.
589 * @param end Last data point value in a series.
590 * @param last Left value
591 * @param current Right value
592 *
593 * @param count
594 * @return true, if a gap is detected between last and current - otherwise
595 * false.
596 */
597 protected boolean specialDetection(
598 double start,
599 double end,
600 double last,
601 double current,
602 int count
603 ) {
604 double delta = Math.abs(end - start);
605 double smallDelta = Math.abs(current - last);
606
607 return (smallDelta > (3.0 / (count - 1) * delta));
608 }
609
610
611 /**
612 * Method used to detect gaps between two data points grids. If the delta
613 * between current and last is bigger than <code>GAP_MAX_LEVEL</code>, a gap
614 * is detected.
615 *
616 * @param last Left value
617 * @param current Right value
618 *
619 * @return True, if a gap was detected - otherwise false.
620 */
621 protected boolean gridDetection(double last, double current) {
622 if (log.isDebugEnabled()) {
623 log.debug("######################################################");
624 log.debug("Parameters for gap detection");
625 log.debug("Defined gap size for grids: " + GAP_MAX_LEVEL);
626 log.debug("1st value to compare: " + last);
627 log.debug("2nd value to compare: " + current);
628 log.debug("Difference: " + Math.abs(current - last));
629 }
630 return (Math.abs(current - last) > GAP_MAX_LEVEL);
631 }
632
633
634 /**
635 * This method returns the key which is used to retrieve the y-value served
636 * by a <code>Result</code> object.
637 *
638 * @param first <code>Result</code> object - not used in this class.
639 * @param second <code>Result</code> object - not used in this class.
640 *
641 * @return the string "KPOSITION"
642 */
643 protected String getDependendAxisName(Result first, Result second) {
644 return "KPOSITION";
645 }
646 }
647 // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :

http://dive4elements.wald.intevation.org