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