Mercurial > dive4elements > gnv-client
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 > 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 > (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 : |