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

http://dive4elements.wald.intevation.org