comparison artifacts/src/main/java/org/dive4elements/river/exports/AbstractChartGenerator.java @ 9123:1cc7653ca84f

Cleanup of ChartGenerator and ChartGenerator2 code. Put some of the copy/pasted code into a common abstraction.
author gernotbelger
date Tue, 05 Jun 2018 19:21:16 +0200
parents 07d51fd4864c
children eec4df8165a1
comparison
equal deleted inserted replaced
9122:b8e7f6becf78 9123:1cc7653ca84f
7 * and comes with ABSOLUTELY NO WARRANTY! Check out the 7 * and comes with ABSOLUTELY NO WARRANTY! Check out the
8 * documentation coming with Dive4Elements River for details. 8 * documentation coming with Dive4Elements River for details.
9 */ 9 */
10 package org.dive4elements.river.exports; 10 package org.dive4elements.river.exports;
11 11
12 import java.awt.BasicStroke;
13 import java.awt.Color;
14 import java.awt.Font;
15 import java.awt.Paint;
16 import java.awt.Stroke;
17 import java.awt.TexturePaint;
18 import java.awt.geom.Rectangle2D;
19 import java.awt.image.BufferedImage;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.text.DateFormat;
23 import java.util.ArrayList;
24 import java.util.Date;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Map;
28 import java.util.SortedMap;
29 import java.util.TreeMap;
30
12 import javax.xml.xpath.XPathConstants; 31 import javax.xml.xpath.XPathConstants;
13 32
33 import org.apache.log4j.Logger;
34 import org.dive4elements.artifactdatabase.state.ArtifactAndFacet;
35 import org.dive4elements.artifactdatabase.state.Settings;
36 import org.dive4elements.artifacts.Artifact;
14 import org.dive4elements.artifacts.ArtifactNamespaceContext; 37 import org.dive4elements.artifacts.ArtifactNamespaceContext;
15 import org.dive4elements.artifacts.CallContext; 38 import org.dive4elements.artifacts.CallContext;
39 import org.dive4elements.artifacts.CallMeta;
40 import org.dive4elements.artifacts.PreferredLocale;
16 import org.dive4elements.artifacts.common.utils.XMLUtils; 41 import org.dive4elements.artifacts.common.utils.XMLUtils;
42 import org.dive4elements.river.FLYS;
17 import org.dive4elements.river.artifacts.D4EArtifact; 43 import org.dive4elements.river.artifacts.D4EArtifact;
18 import org.dive4elements.river.artifacts.access.RangeAccess; 44 import org.dive4elements.river.artifacts.access.RangeAccess;
19 import org.dive4elements.river.artifacts.access.RiverAccess; 45 import org.dive4elements.river.artifacts.access.RiverAccess;
46 import org.dive4elements.river.artifacts.resources.Resources;
47 import org.dive4elements.river.artifacts.sinfo.util.CalculationUtils;
48 import org.dive4elements.river.collections.D4EArtifactCollection;
49 import org.dive4elements.river.jfree.AxisDataset;
50 import org.dive4elements.river.jfree.Bounds;
51 import org.dive4elements.river.jfree.DoubleBounds;
52 import org.dive4elements.river.jfree.EnhancedLineAndShapeRenderer;
53 import org.dive4elements.river.jfree.RiverAnnotation;
54 import org.dive4elements.river.jfree.StableXYDifferenceRenderer;
55 import org.dive4elements.river.jfree.Style;
56 import org.dive4elements.river.jfree.StyledAreaSeriesCollection;
57 import org.dive4elements.river.jfree.StyledSeries;
58 import org.dive4elements.river.themes.ThemeDocument;
59 import org.dive4elements.river.utils.Formatter;
20 import org.jfree.chart.JFreeChart; 60 import org.jfree.chart.JFreeChart;
61 import org.jfree.chart.LegendItem;
62 import org.jfree.chart.LegendItemCollection;
63 import org.jfree.chart.axis.NumberAxis;
64 import org.jfree.chart.plot.XYPlot;
65 import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
21 import org.jfree.chart.title.TextTitle; 66 import org.jfree.chart.title.TextTitle;
67 import org.jfree.data.Range;
68 import org.jfree.data.general.Series;
69 import org.jfree.data.xy.XYDataset;
70 import org.jfree.ui.RectangleInsets;
22 import org.w3c.dom.Document; 71 import org.w3c.dom.Document;
72 import org.w3c.dom.Element;
23 73
24 /** 74 /**
75 * This class re-unites the tremendous copy/paste code from ChartGenerator and ChartGenerator2. Code is still awful and
76 * encapsulation is broken in too many places.
77 * TODO: instead of deep inheritances, delegate to classes that define the various behaviors.
78 *
25 * @author Gernot Belger 79 * @author Gernot Belger
26 */ 80 */
27 // FIXME: this class is intended to contain all duplicate code from ChartGenerator and ChartGenerator2; who will clean
28 // up this mess...?
29 abstract class AbstractChartGenerator implements OutGenerator { 81 abstract class AbstractChartGenerator implements OutGenerator {
82
83 protected static final Logger log = Logger.getLogger(AbstractChartGenerator.class);
84
85 private static final int DEFAULT_CHART_WIDTH = 600;
86
87 private static final int DEFAULT_CHART_HEIGHT = 400;
88
89 private static final Color DEFAULT_GRID_COLOR = Color.GRAY;
90
91 private static final float DEFAULT_GRID_LINE_WIDTH = 0.3f;
92
93 protected static final String DEFAULT_FONT_NAME = "Tahoma";
94
95 protected static final int DEFAULT_FONT_SIZE = 12;
96
97 private static final String DEFAULT_CHART_FORMAT = "png";
98
30 private static final String XPATH_CHART_EXPORT = "/art:action/art:attributes/art:export/@art:value"; 99 private static final String XPATH_CHART_EXPORT = "/art:action/art:attributes/art:export/@art:value";
31 100
32 // TODO: move real code here 101 private static final String XPATH_CHART_SIZE = "/art:action/art:attributes/art:size";
33 protected abstract D4EArtifact getArtifact(); 102
103 private static final String XPATH_CHART_FORMAT = "/art:action/art:attributes/art:format/@art:value";
104
105 private static final String XPATH_CHART_X_RANGE = "/art:action/art:attributes/art:xrange";
106
107 private static final String XPATH_CHART_Y_RANGE = "/art:action/art:attributes/art:yrange";
108
109 /** The document of the incoming out() request. */
110 private Document request;
111
112 /** The output stream where the data should be written to. */
113 private OutputStream out;
114
115 /** Artifact that is used to decorate the chart with meta information. */
116 private Artifact master;
117
118 /** Map of datasets ("index"). */
119 private final SortedMap<Integer, AxisDataset> datasets = new TreeMap<>();
120
121 /** List of annotations to insert in plot. */
122 private final List<RiverAnnotation> annotations = new ArrayList<>();
123
124 private String outName;
125
126 /** The settings that should be used during output creation. */
127 private Settings settings;
34 128
35 /** The CallContext object. */ 129 /** The CallContext object. */
36 // TODO: move real code here 130 private CallContext context;
37 protected abstract CallContext getContext(); 131
132 @Override
133 public void init(final String outName, final Document request, final OutputStream out, final CallContext context) {
134 log.debug("ChartGenerator.init");
135
136 this.outName = outName;
137 this.request = request;
138 this.out = out;
139 this.context = context;
140 }
141
142 @Override
143 public void setup(final Object config) {
144 }
145
146 /** Sets the master artifact. */
147 @Override
148 public void setMasterArtifact(final Artifact master) {
149 this.master = master;
150 }
151
152 /**
153 * Gets the master artifact.
154 *
155 * @return the master artifact.
156 */
157 public Artifact getMaster() {
158 return this.master;
159 }
160
161 protected final Map<Integer, AxisDataset> getDatasets() {
162 return this.datasets;
163 }
164
165 @Override
166 public void setCollection(final D4EArtifactCollection collection) {
167 /* we do not need it */
168 }
169
170 protected final D4EArtifact getArtifact() {
171 // FIXME: should already made sure when this member is set
172 return (D4EArtifact) this.master;
173 }
174
175 public final CallContext getContext() {
176 return this.context;
177 }
38 178
39 /** The document of the incoming out() request. */ 179 /** The document of the incoming out() request. */
40 // TODO: move real code here 180 protected final Document getRequest() {
41 protected abstract Document getRequest(); 181 return this.request;
182 }
183
184 /**
185 * Adds annotations to list. The given annotation will be visible.
186 */
187 public final void addAnnotations(final RiverAnnotation annotation) {
188 this.annotations.add(annotation);
189 }
190
191 /**
192 * This method needs to be implemented by concrete subclasses to create new
193 * instances of JFreeChart.
194 *
195 * @param context2
196 *
197 * @return a new instance of a JFreeChart.
198 */
199 protected abstract JFreeChart generateChart(CallContext context2);
200
201 /**
202 * For every outable (i.e. facets), this function is
203 * called and handles the data accordingly.
204 */
205 @Override
206 public abstract void doOut(ArtifactAndFacet bundle, ThemeDocument attr, boolean visible);
207
208 @Override
209 public void generate() throws IOException {
210 doGenerate(this.context, this.out, this.outName);
211 }
212
213 protected abstract void doGenerate(CallContext context, OutputStream out, String outName) throws IOException;
214
215 protected abstract Series getSeriesOf(XYDataset dataset, int idx);
216
217 /**
218 * Returns the default title of a chart.
219 *
220 * @param context2
221 *
222 * @return the default title of a chart.
223 */
224 protected abstract String getDefaultChartTitle(CallContext context);
225
226 /**
227 * This method is used to create new AxisDataset instances which may differ
228 * in concrete subclasses.
229 *
230 * @param idx
231 * The index of an axis.
232 */
233 protected abstract AxisDataset createAxisDataset(int idx);
234
235 /**
236 * Combines the ranges of the X axis at index <i>idx</i>.
237 *
238 * @param bounds
239 * A new Bounds.
240 * @param idx
241 * The index of the X axis that should be comined with
242 * <i>range</i>.
243 */
244 protected abstract void combineXBounds(Bounds bounds, int idx);
245
246 /**
247 * Combines the ranges of the Y axis at index <i>idx</i>.
248 *
249 * @param bounds
250 * A new Bounds.
251 * @param index
252 * The index of the Y axis that should be comined with.
253 * <i>range</i>.
254 */
255 protected abstract void combineYBounds(Bounds bounds, int index);
256
257 /**
258 * This method is used to determine the ranges for axes at a given index.
259 *
260 * @param index
261 * The index of the axes at the plot.
262 *
263 * @return a Range[] with [xrange, yrange];
264 */
265 protected abstract Range[] getRangesForAxis(int index);
266
267 protected abstract Bounds getXBounds(int axis);
268
269 protected abstract void setXBounds(int axis, Bounds bounds);
270
271 protected abstract Bounds getYBounds(int axis);
272
273 protected abstract void setYBounds(int axis, Bounds bounds);
274
275 // /**
276 // * Retuns the call context. May be null if init hasn't been called yet.
277 // *
278 // * @return the CallContext instance
279 // */
280 // protected final CallContext getCallContext() {
281 // return this.context;
282 // }
283 //
284
285 @Override
286 public final void setSettings(final Settings settings) {
287 this.settings = settings;
288 }
289
290 /**
291 * Returns the <i>settings</i> as <i>ChartSettings</i>.
292 *
293 * @return the <i>settings</i> as <i>ChartSettings</i> or null, if
294 * <i>settings</i> is not an instance of <i>ChartSettings</i>.
295 */
296 protected final ChartSettings getChartSettings() {
297 if (this.settings instanceof ChartSettings) {
298 return (ChartSettings) this.settings;
299 }
300
301 return null;
302 }
303
304 /**
305 * Return instance of <i>ChartSettings</i> with a chart specific section
306 * but with no axes settings.
307 *
308 * @return an instance of <i>ChartSettings</i>.
309 */
310 @Override
311 public final Settings getSettings() {
312 if (this.settings != null)
313 return this.settings;
314
315 final ChartSettings settings = new ChartSettings();
316
317 final ChartSection chartSection = buildChartSection(this.context);
318 final LegendSection legendSection = buildLegendSection();
319 final ExportSection exportSection = buildExportSection();
320
321 settings.setChartSection(chartSection);
322 settings.setLegendSection(legendSection);
323 settings.setExportSection(exportSection);
324
325 final List<AxisSection> axisSections = buildAxisSections();
326 for (final AxisSection axisSection : axisSections)
327 settings.addAxisSection(axisSection);
328
329 return settings;
330 }
331
332 protected abstract ChartSection buildChartSection(CallContext context);
333
334 /**
335 * Creates a new <i>LegendSection</i>.
336 *
337 * @return a new <i>LegendSection</i>.
338 */
339 private LegendSection buildLegendSection() {
340 final LegendSection legendSection = new LegendSection();
341 legendSection.setVisibility(isLegendVisible());
342 legendSection.setFontSize(getLegendFontSize());
343 legendSection.setAggregationThreshold(10);
344 return legendSection;
345 }
346
347 /**
348 * Creates a new <i>ExportSection</i> with default values <b>WIDTH=600</b>
349 * and <b>HEIGHT=400</b>.
350 *
351 * @return a new <i>ExportSection</i>.
352 */
353 private ExportSection buildExportSection() {
354 final ExportSection exportSection = new ExportSection();
355 exportSection.setWidth(DEFAULT_CHART_WIDTH);
356 exportSection.setHeight(DEFAULT_CHART_HEIGHT);
357 exportSection.setMetadata(true);
358 return exportSection;
359 }
360
361 /**
362 * Creates a list of Sections that contains all axes of the chart (including
363 * X and Y axes).
364 *
365 * @return a list of Sections for each axis in this chart.
366 */
367 private List<AxisSection> buildAxisSections() {
368 final List<AxisSection> axisSections = new ArrayList<>();
369
370 axisSections.addAll(buildXAxisSections());
371 axisSections.addAll(buildYAxisSections());
372
373 return axisSections;
374 }
375
376 /**
377 * Creates a new Section for chart's X axis.
378 *
379 * @return a List that contains a Section for the X axis.
380 */
381 protected List<AxisSection> buildXAxisSections() {
382 final List<AxisSection> axisSections = new ArrayList<>();
383
384 final String identifier = "X";
385
386 final AxisSection axisSection = new AxisSection();
387 axisSection.setIdentifier(identifier);
388 axisSection.setLabel(getXAxisLabel());
389 axisSection.setFontSize(14);
390 axisSection.setFixed(false);
391
392 // XXX We are able to find better default ranges that [0,0], but the Y
393 // axes currently have no better ranges set.
394 axisSection.setUpperRange(0d);
395 axisSection.setLowerRange(0d);
396
397 axisSections.add(axisSection);
398
399 return axisSections;
400 }
401
402 /**
403 * Returns the X-Axis label of a chart.
404 *
405 * @return the X-Axis label of a chart.
406 */
407 protected final String getXAxisLabel() {
408 final ChartSettings chartSettings = getChartSettings();
409 if (chartSettings == null) {
410 return getDefaultXAxisLabel(this.context);
411 }
412
413 final AxisSection as = chartSettings.getAxisSection("X");
414 if (as != null) {
415 final String label = as.getLabel();
416
417 if (label != null) {
418 return label;
419 }
420 }
421
422 return getDefaultXAxisLabel(this.context);
423 }
424
425 protected abstract List<AxisSection> buildYAxisSections();
426
427 /**
428 * Returns the default X-Axis label of a chart.
429 *
430 * @param context2
431 *
432 * @return the default X-Axis label of a chart.
433 */
434 protected abstract String getDefaultXAxisLabel(final CallContext context2);
435
436 /** Generate the diagram as an image. */
437 protected final void generateImage(final CallContext context) throws IOException {
438 log.debug("ChartGenerator2.generateImage");
439
440 final JFreeChart chart = generateChart(context);
441
442 final String format = getFormat();
443 int[] size = getSize();
444
445 if (size == null)
446 size = getExportDimension();
447
448 this.context.putContextValue("chart.width", size[0]);
449 this.context.putContextValue("chart.height", size[1]);
450
451 if (format.equals(ChartExportHelper.FORMAT_PNG)) {
452 this.context.putContextValue("chart.image.format", "png");
453
454 ChartExportHelper.exportImage(this.out, chart, this.context);
455 } else if (format.equals(ChartExportHelper.FORMAT_PDF)) {
456 preparePDFContext(this.context);
457
458 ChartExportHelper.exportPDF(this.out, chart, this.context);
459 } else if (format.equals(ChartExportHelper.FORMAT_SVG)) {
460 prepareSVGContext(this.context);
461
462 ChartExportHelper.exportSVG(this.out, chart, this.context);
463 } else if (format.equals(ChartExportHelper.FORMAT_CSV)) {
464 this.context.putContextValue("chart.image.format", "csv");
465
466 ChartExportHelper.exportCSV(this.out, chart, this.context);
467 }
468 }
42 469
43 /** 470 /**
44 * Adds a metadata sub-title to the chart if it gets exported 471 * Adds a metadata sub-title to the chart if it gets exported
45 */ 472 */
46 protected final void addMetadataSubtitle(final JFreeChart chart) { 473 protected final void addMetadataSubtitle(final CallContext context, final JFreeChart chart) {
47 if (isExport()) { 474 if ((!isExport() || !isExportMetadata()))
48 final String text = ChartExportHelper.createMetadataSubtitle(getArtifact(), getContext(), getRiverName()); 475 return;
49 chart.addSubtitle(new TextTitle(text)); 476
50 } 477 final String version = FLYS.VERSION;
478 final String user = CalculationUtils.findArtifactUser(context, getArtifact());
479 final Locale locale = Resources.getLocale(context.getMeta());
480 final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
481 final String dateText = df.format(new Date());
482
483 final String text = Resources.getMsg(context.getMeta(), "chart.subtitle.metadata", "default", version, user, dateText);
484
485 chart.addSubtitle(new TextTitle(text));
486 }
487
488 private boolean isExportMetadata() {
489 final ChartSettings chartSettings = getChartSettings();
490 if (chartSettings == null)
491 return true;
492
493 final ExportSection exportSection = chartSettings.getExportSection();
494 if (exportSection == null)
495 return true;
496
497 return exportSection.getMetadata();
51 } 498 }
52 499
53 /** 500 /**
54 * This method returns the export flag specified in the <i>request</i> document 501 * This method returns the export flag specified in the <i>request</i> document
55 * or <i>false</i> if no export is specified in <i>request</i>. 502 * or <i>false</i> if no export is specified in <i>request</i>.
56 */ 503 */
57 protected final boolean isExport() { 504 private boolean isExport() {
58 final Boolean export = (Boolean) XMLUtils.xpath(getRequest(), XPATH_CHART_EXPORT, XPathConstants.BOOLEAN, ArtifactNamespaceContext.INSTANCE); 505 final Boolean export = (Boolean) XMLUtils.xpath(getRequest(), XPATH_CHART_EXPORT, XPathConstants.BOOLEAN, ArtifactNamespaceContext.INSTANCE);
59 506
60 return export == null ? false : export; 507 return export == null ? false : export;
61 } 508 }
62 509
72 final D4EArtifact flys = getArtifact(); 519 final D4EArtifact flys = getArtifact();
73 520
74 final RangeAccess rangeAccess = new RangeAccess(flys); 521 final RangeAccess rangeAccess = new RangeAccess(flys);
75 return rangeAccess.getKmRange(); 522 return rangeAccess.getKmRange();
76 } 523 }
524
525 /**
526 * Returns a boolean object that determines if the chart grid should be
527 * visible or not. This information needs to be provided by <i>settings</i>,
528 * otherwise the default is true.
529 *
530 * @param settings
531 * A ChartSettings object.
532 *
533 * @return true, if the chart grid should be visible otherwise false.
534 *
535 * @throws NullPointerException
536 * if <i>settings</i> is null.
537 */
538 private boolean isGridVisible(final ChartSettings settings) {
539 final ChartSection cs = settings.getChartSection();
540 return cs.getDisplayGrid();
541 }
542
543 /**
544 * This method is used to determine, if the chart's legend is visible or
545 * not. If a <i>settings</i> instance is set, this instance determines the
546 * visibility otherwise, this method returns true as default if no
547 * <i>settings</i> is set.
548 *
549 * @return true, if the legend should be visible, otherwise false.
550 */
551 protected final boolean isLegendVisible() {
552 final ChartSettings chartSettings = getChartSettings();
553 if (chartSettings == null)
554 return true;
555
556 final LegendSection ls = chartSettings.getLegendSection();
557 return ls.getVisibility();
558 }
559
560 /**
561 * This method returns the font size for the X axis. If the font size is
562 * specified in ChartSettings (if <i>chartSettings</i> is set), this size is
563 * returned. Otherwise the default font size 12 is returned.
564 *
565 * @return the font size for the x axis.
566 */
567 protected final int getXAxisLabelFontSize() {
568 final ChartSettings chartSettings = getChartSettings();
569 if (chartSettings == null) {
570 return DEFAULT_FONT_SIZE;
571 }
572
573 final AxisSection as = chartSettings.getAxisSection("X");
574 final Integer fontSize = as.getFontSize();
575
576 return fontSize != null ? fontSize : DEFAULT_FONT_SIZE;
577 }
578
579 /**
580 * This method is used to determine the font size of the chart's legend. If
581 * a <i>settings</i> instance is set, this instance determines the font
582 * size, otherwise this method returns 12 as default if no <i>settings</i>
583 * is set or if it doesn't provide a legend font size.
584 *
585 * @return a legend font size.
586 */
587 private int getLegendFontSize() {
588
589 final ChartSettings chartSettings = getChartSettings();
590 if (chartSettings == null)
591 return DEFAULT_FONT_SIZE;
592
593 final LegendSection ls = chartSettings.getLegendSection();
594 if (ls == null)
595 return DEFAULT_FONT_SIZE;
596
597 final Integer fontSize = ls.getFontSize();
598 if (fontSize == null)
599 return DEFAULT_FONT_SIZE;
600
601 return fontSize;
602 }
603
604 /**
605 * Creates a new LegendItem with <i>name</i> and font provided by
606 * <i>createLegendLabelFont()</i>.
607 *
608 * @param theme
609 * The theme of the chart line.
610 * @param name
611 * The displayed name of the item.
612 *
613 * @return a new LegendItem instance.
614 */
615 protected final LegendItem createLegendItem(final ThemeDocument theme, final String name) {
616 // OPTIMIZE Pass font, parsed Theme items.
617
618 Color color = theme.parseLineColorField();
619 if (color == null)
620 color = Color.BLACK;
621
622 final LegendItem legendItem = new LegendItem(name, color);
623 legendItem.setLabelFont(createLegendLabelFont());
624 return legendItem;
625 }
626
627 /**
628 * Create new legend entries, dependent on settings.
629 *
630 * @param plot
631 * The plot for which to modify the legend.
632 */
633 protected final void aggregateLegendEntries(final XYPlot plot) {
634
635 final ChartSettings chartSettings = getChartSettings();
636 if (chartSettings == null)
637 return;
638
639 final Integer threshold = chartSettings.getLegendSection().getAggregationThreshold();
640
641 final int aggrThreshold = threshold != null ? threshold.intValue() : 0;
642
643 LegendProcessor.aggregateLegendEntries(plot, aggrThreshold);
644 }
645
646 /**
647 * Creates Font (Family and size) to use when creating Legend Items. The
648 * font size depends in the return value of <i>getLegendFontSize()</i>.
649 *
650 * @return a new Font instance with <i>DEFAULT_FONT_NAME</i>.
651 */
652 private final Font createLegendLabelFont() {
653 return new Font(DEFAULT_FONT_NAME, Font.PLAIN, getLegendFontSize());
654 }
655
656 /**
657 * Adjust some Stroke/Grid parameters for <i>plot</i>. The chart
658 * <i>Settings</i> are applied in this method.
659 *
660 * @param plot
661 * The XYPlot which is adapted.
662 */
663 protected void adjustPlot(final XYPlot plot) {
664 final Stroke gridStroke = new BasicStroke(DEFAULT_GRID_LINE_WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 3.0f, new float[] { 3.0f }, 0.0f);
665
666 final ChartSettings cs = getChartSettings();
667 final boolean isGridVisible = cs != null ? isGridVisible(cs) : true;
668
669 plot.setDomainGridlineStroke(gridStroke);
670 plot.setDomainGridlinePaint(DEFAULT_GRID_COLOR);
671 plot.setDomainGridlinesVisible(isGridVisible);
672
673 plot.setRangeGridlineStroke(gridStroke);
674 plot.setRangeGridlinePaint(DEFAULT_GRID_COLOR);
675 plot.setRangeGridlinesVisible(isGridVisible);
676
677 plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));
678 }
679
680 /**
681 * This helper method is used to extract the current locale from instance variable <i>context</i>.
682 *
683 * @return the current locale.
684 */
685 protected final Locale getLocale() {
686 final CallMeta meta = this.context.getMeta();
687 final PreferredLocale[] prefs = meta.getLanguages();
688
689 final int len = prefs != null ? prefs.length : 0;
690
691 final Locale[] locales = new Locale[len];
692
693 for (int i = 0; i < len; i++) {
694 locales[i] = prefs[i].getLocale();
695 }
696
697 return meta.getPreferredLocale(locales);
698 }
699
700 /**
701 * Look up \param key in i18n dictionary.
702 *
703 * @param key
704 * key for which to find i18nd version.
705 * @param def
706 * default, returned if lookup failed.
707 * @return value found in i18n dictionary, \param def if no value found.
708 */
709 public final String msg(final String key, final String def) {
710 return Resources.getMsg(this.context.getMeta(), key, def);
711 }
712
713 /**
714 * Look up \param key in i18n dictionary.
715 *
716 * @param key
717 * key for which to find i18nd version.
718 * @return value found in i18n dictionary, key itself if failed.
719 */
720 public final String msg(final String key) {
721 return Resources.getMsg(this.context.getMeta(), key, key);
722 }
723
724 public final String msg(final String key, final String def, final Object[] args) {
725 return Resources.getMsg(this.context.getMeta(), key, def, args);
726 }
727
728 /**
729 * Add datasets stored in instance variable <i>datasets</i> to plot.
730 * <i>datasets</i> actually stores instances of AxisDataset, so each of this
731 * datasets is mapped to a specific axis as well.
732 *
733 * @param plot
734 * plot to add datasets to.
735 */
736 protected void addDatasets(final XYPlot plot) {
737 log.debug("addDatasets()");
738
739 // AxisDatasets are sorted, but some might be empty.
740 // Thus, generate numbering on the fly.
741 int axisIndex = 0;
742 int datasetIndex = 0;
743
744 for (final Map.Entry<Integer, AxisDataset> entry : this.datasets.entrySet()) {
745 if (!entry.getValue().isEmpty()) {
746 // Add axis and range information.
747 final AxisDataset axisDataset = entry.getValue();
748 final NumberAxis axis = createYAxis(entry.getKey());
749
750 plot.setRangeAxis(axisIndex, axis);
751
752 if (axis.getAutoRangeIncludesZero()) {
753 axisDataset.setRange(Range.expandToInclude(axisDataset.getRange(), 0d));
754 }
755
756 setYBounds(axisIndex, expandPointRange(axisDataset.getRange()));
757
758 // Add contained datasets, mapping to axis.
759 for (final XYDataset dataset : axisDataset.getDatasets()) {
760 try {
761 plot.setDataset(datasetIndex, dataset);
762 plot.mapDatasetToRangeAxis(datasetIndex, axisIndex);
763
764 applyThemes(plot, dataset, datasetIndex, axisDataset.isArea(dataset));
765
766 datasetIndex++;
767 }
768 catch (final RuntimeException re) {
769 log.error(re);
770 }
771 }
772
773 axisDataset.setPlotAxisIndex(axisIndex);
774 axisIndex++;
775 }
776 }
777 }
778
779 /**
780 * Create Y (range) axis for given index.
781 * Shall be implemented by subclasses.
782 */
783 protected abstract NumberAxis createYAxis(final int index);
784
785 /**
786 * @param idx
787 * "index" of dataset/series (first dataset to be drawn has
788 * index 0), correlates with renderer index.
789 * @param isArea
790 * true if the series describes an area and shall be rendered
791 * as such.
792 */
793 private void applyThemes(final XYPlot plot, final XYDataset series, final int idx, final boolean isArea) {
794 if (isArea) {
795 applyAreaTheme(plot, (StyledAreaSeriesCollection) series, idx);
796 } else {
797 applyLineTheme(plot, series, idx);
798 }
799 }
800
801 /**
802 * Expands a given range if it collapses into one point.
803 *
804 * @param range
805 * Range to be expanded if upper == lower bound.
806 *
807 * @return Bounds of point plus 5 percent in each direction.
808 */
809 private Bounds expandPointRange(final Range range) {
810 if (range == null) {
811 return null;
812 } else if (range.getLowerBound() == range.getUpperBound()) {
813 final Range expandedRange = ChartHelper.expandRange(range, 5d);
814 return new DoubleBounds(expandedRange.getLowerBound(), expandedRange.getUpperBound());
815 }
816
817 return new DoubleBounds(range.getLowerBound(), range.getUpperBound());
818 }
819
820 /**
821 * Creates a new instance of EnhancedLineAndShapeRenderer.
822 *
823 * @param plot
824 * The plot which is set for the new renderer.
825 * @param idx
826 * This value is not used in the current implementation.
827 *
828 * @return a new instance of EnhancedLineAndShapeRenderer.
829 */
830 private XYLineAndShapeRenderer createRenderer(final XYPlot plot, final int idx) {
831 log.debug("Create EnhancedLineAndShapeRenderer for idx: " + idx);
832
833 final EnhancedLineAndShapeRenderer r = new EnhancedLineAndShapeRenderer(true, false);
834
835 r.setPlot(plot);
836
837 return r;
838 }
839
840 /**
841 * This method applies the themes defined in the series itself. Therefore,
842 * <i>StyledXYSeries.applyTheme()</i> is called, which modifies the renderer
843 * for the series.
844 *
845 * @param plot
846 * The plot.
847 * @param dataset
848 * The XYDataset which needs to support Series objects.
849 * @param idx
850 * The index of the renderer / dataset.
851 */
852 private void applyLineTheme(final XYPlot plot, final XYDataset dataset, final int idx) {
853 log.debug("Apply LineTheme for dataset at index: " + idx);
854
855 final LegendItemCollection lic = new LegendItemCollection();
856 final LegendItemCollection anno = plot.getFixedLegendItems();
857
858 final Font legendFont = createLegendLabelFont();
859
860 final XYLineAndShapeRenderer renderer = createRenderer(plot, idx);
861
862 for (int s = 0, num = dataset.getSeriesCount(); s < num; s++) {
863 final Series series = getSeriesOf(dataset, s);
864
865 if (series instanceof StyledSeries) {
866 final Style style = ((StyledSeries) series).getStyle();
867 style.applyTheme(renderer, s);
868 }
869
870 // special case: if there is just one single item, we need to enable
871 // points for this series, otherwise we would not see anything in
872 // the chart area.
873 if (series.getItemCount() == 1) {
874 renderer.setSeriesShapesVisible(s, true);
875 }
876
877 LegendItem legendItem = renderer.getLegendItem(idx, s);
878 if (legendItem.getLabel().endsWith(" ") || legendItem.getLabel().endsWith("interpol")) {
879 legendItem = null;
880 }
881
882 if (legendItem != null) {
883 legendItem.setLabelFont(legendFont);
884 lic.add(legendItem);
885 } else {
886 log.warn("Could not get LegentItem for renderer: " + idx + ", series-idx " + s);
887 }
888 }
889
890 if (anno != null) {
891 lic.addAll(anno);
892 }
893
894 plot.setFixedLegendItems(lic);
895
896 plot.setRenderer(idx, renderer);
897 }
898
899 /**
900 * @param plot
901 * The plot.
902 * @param area
903 * A StyledAreaSeriesCollection object.
904 * @param idx
905 * The index of the dataset.
906 */
907 private final void applyAreaTheme(final XYPlot plot, final StyledAreaSeriesCollection area, final int idx) {
908 final LegendItemCollection lic = new LegendItemCollection();
909 final LegendItemCollection anno = plot.getFixedLegendItems();
910
911 final Font legendFont = createLegendLabelFont();
912
913 log.debug("Registering an 'area'renderer at idx: " + idx);
914
915 final StableXYDifferenceRenderer dRenderer = new StableXYDifferenceRenderer();
916
917 if (area.getMode() == StyledAreaSeriesCollection.FILL_MODE.UNDER) {
918 dRenderer.setPositivePaint(createTransparentPaint());
919 }
920
921 plot.setRenderer(idx, dRenderer);
922
923 area.applyTheme(dRenderer);
924
925 // i18n
926 dRenderer.setAreaLabelNumberFormat(Formatter.getFormatter(this.context.getMeta(), 2, 4));
927
928 dRenderer.setAreaLabelTemplate(Resources.getMsg(this.context.getMeta(), "area.label.template", "Area=%sm2"));
929
930 final LegendItem legendItem = dRenderer.getLegendItem(idx, 0);
931 if (legendItem != null) {
932 legendItem.setLabelFont(legendFont);
933 lic.add(legendItem);
934 } else {
935 log.warn("Could not get LegentItem for renderer: " + idx + ", series-idx " + 0);
936 }
937
938 if (anno != null) {
939 lic.addAll(anno);
940 }
941
942 plot.setFixedLegendItems(lic);
943 }
944
945 /**
946 * Returns a transparently textured paint.
947 *
948 * @return a transparently textured paint.
949 */
950 private static Paint createTransparentPaint() {
951 // TODO why not use a transparent color?
952 final BufferedImage texture = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR);
953
954 return new TexturePaint(texture, new Rectangle2D.Double(0d, 0d, 0d, 0d));
955 }
956
957 private void preparePDFContext(final CallContext context) {
958 final int[] dimension = getExportDimension();
959
960 context.putContextValue("chart.width", dimension[0]);
961 context.putContextValue("chart.height", dimension[1]);
962 context.putContextValue("chart.marginLeft", 5f);
963 context.putContextValue("chart.marginRight", 5f);
964 context.putContextValue("chart.marginTop", 5f);
965 context.putContextValue("chart.marginBottom", 5f);
966 context.putContextValue("chart.page.format", ChartExportHelper.DEFAULT_PAGE_SIZE);
967 }
968
969 private void prepareSVGContext(final CallContext context) {
970 final int[] dimension = getExportDimension();
971
972 context.putContextValue("chart.width", dimension[0]);
973 context.putContextValue("chart.height", dimension[1]);
974 context.putContextValue("chart.encoding", ChartExportHelper.DEFAULT_ENCODING);
975 }
976
977 /**
978 * This method retrieves the chart subtitle by calling getChartSubtitle()
979 * and adds it as TextTitle to the chart.
980 * The default implementation of getChartSubtitle() returns the same
981 * as getDefaultChartSubtitle() which must be implemented by derived
982 * classes. If you want to add multiple subtitles to the chart override
983 * this method and add your subtitles manually.
984 *
985 * @param chart
986 * The JFreeChart chart object.
987 */
988 protected void addSubtitles(final CallContext context, final JFreeChart chart) {
989 final String subtitle = getChartSubtitle(this.context);
990
991 if (subtitle != null && subtitle.length() > 0) {
992 chart.addSubtitle(new TextTitle(subtitle));
993 }
994 }
995
996 protected abstract String getChartSubtitle(CallContext context);
997
998 /**
999 * Adds a new AxisDataset which contains <i>dataset</i> at index <i>idx</i>.
1000 *
1001 * @param dataset
1002 * An XYDataset.
1003 * @param idx
1004 * The axis index.
1005 * @param visible
1006 * Determines, if the dataset should be visible or not.
1007 */
1008 protected final void addAxisDataset(final XYDataset dataset, final int idx, final boolean visible) {
1009 if (dataset == null || idx < 0) {
1010 return;
1011 }
1012
1013 final AxisDataset axisDataset = getAxisDataset(idx);
1014
1015 final Bounds[] xyBounds = ChartHelper.getBounds(dataset);
1016
1017 if (xyBounds == null) {
1018 log.warn("Skip XYDataset for Axis (invalid ranges): " + idx);
1019 return;
1020 }
1021
1022 if (visible) {
1023 if (log.isDebugEnabled()) {
1024 log.debug("Add new AxisDataset at index: " + idx);
1025 log.debug("X extent: " + xyBounds[0]);
1026 log.debug("Y extent: " + xyBounds[1]);
1027 }
1028
1029 axisDataset.addDataset(dataset);
1030 }
1031
1032 combineXBounds(xyBounds[0], 0);
1033 combineYBounds(xyBounds[1], idx);
1034 }
1035
1036 /**
1037 * This method grants access to the AxisDatasets stored in <i>datasets</i>.
1038 * If no AxisDataset exists for index <i>idx</i>, a new AxisDataset is
1039 * created using <i>createAxisDataset()</i>.
1040 *
1041 * @param idx
1042 * The index of the desired AxisDataset.
1043 *
1044 * @return an existing or new AxisDataset.
1045 */
1046 protected final AxisDataset getAxisDataset(final int idx) {
1047 AxisDataset axisDataset = this.datasets.get(idx);
1048
1049 if (axisDataset == null) {
1050 axisDataset = createAxisDataset(idx);
1051 this.datasets.put(idx, axisDataset);
1052 }
1053
1054 return axisDataset;
1055 }
1056
1057 /**
1058 * Returns the size of a chart export as array which has been specified by
1059 * the incoming request document.
1060 *
1061 * @return the size of a chart as [width, height] or null if no width or
1062 * height are given in the request document.
1063 */
1064 protected final int[] getSize() {
1065 final int[] size = new int[2];
1066
1067 final Element sizeEl = (Element) XMLUtils.xpath(this.request, XPATH_CHART_SIZE, XPathConstants.NODE, ArtifactNamespaceContext.INSTANCE);
1068
1069 if (sizeEl != null) {
1070 final String uri = ArtifactNamespaceContext.NAMESPACE_URI;
1071
1072 final String w = sizeEl.getAttributeNS(uri, "width");
1073 final String h = sizeEl.getAttributeNS(uri, "height");
1074
1075 if (w.length() > 0 && h.length() > 0) {
1076 try {
1077 size[0] = Integer.parseInt(w);
1078 size[1] = Integer.parseInt(h);
1079 }
1080 catch (final NumberFormatException nfe) {
1081 log.warn("Wrong values for chart width/height.");
1082 }
1083 }
1084 }
1085
1086 return size[0] > 0 && size[1] > 0 ? size : null;
1087 }
1088
1089 /**
1090 * This method returns the format specified in the <i>request</i> document
1091 * or <i>DEFAULT_CHART_FORMAT</i> if no format is specified in
1092 * <i>request</i>.
1093 *
1094 * @return the format used to export this chart.
1095 */
1096 private String getFormat() {
1097 final String format = (String) XMLUtils.xpath(this.request, XPATH_CHART_FORMAT, XPathConstants.STRING, ArtifactNamespaceContext.INSTANCE);
1098
1099 return format == null || format.length() == 0 ? DEFAULT_CHART_FORMAT : format;
1100 }
1101
1102 /**
1103 * Returns the X-Axis range as String array from request document.
1104 * If the (x|y)range elements are not found in request document, return
1105 * null (i.e. not zoomed).
1106 *
1107 * @return a String array with [lower, upper], null if not in document.
1108 */
1109 protected final String[] getDomainAxisRangeFromRequest() {
1110 final Element xrange = (Element) XMLUtils.xpath(this.request, XPATH_CHART_X_RANGE, XPathConstants.NODE, ArtifactNamespaceContext.INSTANCE);
1111
1112 if (xrange == null) {
1113 return null;
1114 }
1115
1116 final String uri = ArtifactNamespaceContext.NAMESPACE_URI;
1117
1118 final String lower = xrange.getAttributeNS(uri, "from");
1119 final String upper = xrange.getAttributeNS(uri, "to");
1120
1121 return new String[] { lower, upper };
1122 }
1123
1124 /**
1125 * Returns null if the (x|y)range-element was not found in
1126 * request document.
1127 * This usally means that the axis are not manually zoomed, i.e. showing
1128 * full data extent.
1129 */
1130 protected final String[] getValueAxisRangeFromRequest() {
1131 final Element yrange = (Element) XMLUtils.xpath(this.request, XPATH_CHART_Y_RANGE, XPathConstants.NODE, ArtifactNamespaceContext.INSTANCE);
1132
1133 if (yrange == null) {
1134 return null;
1135 }
1136
1137 final String uri = ArtifactNamespaceContext.NAMESPACE_URI;
1138
1139 final String lower = yrange.getAttributeNS(uri, "from");
1140 final String upper = yrange.getAttributeNS(uri, "to");
1141
1142 return new String[] { lower, upper };
1143 }
1144
1145 /**
1146 * Returns the default size of a chart export as array.
1147 *
1148 * @return the default size of a chart as [width, height].
1149 */
1150 protected final int[] getDefaultSize() {
1151 return new int[] { DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT };
1152 }
1153
1154 /**
1155 * This method returns the export dimension specified in ChartSettings as
1156 * int array [width,height].
1157 *
1158 * @return an int array with [width,height].
1159 */
1160 private int[] getExportDimension() {
1161 final ChartSettings chartSettings = getChartSettings();
1162 if (chartSettings == null)
1163 return new int[] { DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT };
1164
1165 final ExportSection export = chartSettings.getExportSection();
1166 final Integer width = export.getWidth();
1167 final Integer height = export.getHeight();
1168
1169 if (width != null && height != null) {
1170 return new int[] { width, height };
1171 }
1172
1173 return new int[] { 600, 400 };
1174 }
1175
1176 /**
1177 * Returns the chart title provided by <i>settings</i>.
1178 *
1179 * @param settings
1180 * A ChartSettings object.
1181 *
1182 * @return the title provided by <i>settings</i> or null if no
1183 * <i>ChartSection</i> is provided by <i>settings</i>.
1184 *
1185 * @throws NullPointerException
1186 * if <i>settings</i> is null.
1187 */
1188 private String getChartTitle(final ChartSettings settings) {
1189 final ChartSection cs = settings.getChartSection();
1190 return cs != null ? cs.getTitle() : null;
1191 }
1192
1193 /**
1194 * Returns the chart subtitle provided by <i>settings</i>.
1195 *
1196 * @param settings
1197 * A ChartSettings object.
1198 *
1199 * @return the subtitle provided by <i>settings</i> or null if no
1200 * <i>ChartSection</i> is provided by <i>settings</i>.
1201 *
1202 * @throws NullPointerException
1203 * if <i>settings</i> is null.
1204 */
1205 protected final String getChartSubtitle(final ChartSettings settings) {
1206 final ChartSection cs = settings.getChartSection();
1207 return cs != null ? cs.getSubtitle() : null;
1208 }
1209
1210 /**
1211 * Returns the title of a chart. The return value depends on the existence
1212 * of ChartSettings: if there are ChartSettings set, this method returns the
1213 * chart title provided by those settings. Otherwise, this method returns
1214 * getDefaultChartTitle().
1215 *
1216 * @return the title of a chart.
1217 */
1218 protected String getChartTitle(final CallContext context) {
1219 final ChartSettings chartSettings = getChartSettings();
1220
1221 if (chartSettings != null) {
1222 return getChartTitle(chartSettings);
1223 }
1224
1225 return getDefaultChartTitle(context);
1226 }
1227
1228 /**
1229 * This method always returns null. Override it in subclasses that require
1230 * subtitles.
1231 *
1232 * @return null.
1233 */
1234 protected String getDefaultChartSubtitle(final CallContext context) {
1235 // Override this method in subclasses
1236 return null;
1237 }
1238
1239 /** Where to place the logo. */
1240 protected final String logoHPlace() {
1241 final ChartSettings chartSettings = getChartSettings();
1242 if (chartSettings != null) {
1243 final ChartSection cs = chartSettings.getChartSection();
1244 final String place = cs.getLogoHPlacement();
1245
1246 return place;
1247 }
1248 return "center";
1249 }
1250
1251 /** Where to place the logo. */
1252 protected final String logoVPlace() {
1253 final ChartSettings chartSettings = getChartSettings();
1254 if (chartSettings != null) {
1255 final ChartSection cs = chartSettings.getChartSection();
1256 final String place = cs.getLogoVPlacement();
1257
1258 return place;
1259 }
1260 return "top";
1261 }
1262
1263 /** Return the logo id from settings. */
1264 private String showLogo(final ChartSettings chartSettings) {
1265 if (chartSettings != null) {
1266 final ChartSection cs = chartSettings.getChartSection();
1267 final String logo = cs.getDisplayLogo();
1268
1269 return logo;
1270 }
1271 return "none";
1272 }
1273
1274 /**
1275 * This method is used to determine if a logo should be added to the plot.
1276 *
1277 * @return logo name (null if none).
1278 */
1279 protected final String showLogo() {
1280 final ChartSettings chartSettings = getChartSettings();
1281 return showLogo(chartSettings);
1282 }
1283
1284 /**
1285 * This method is used to determine if the resulting chart should display
1286 * grid lines or not. <b>Note: this method always returns true!</b>
1287 *
1288 * @return true, if the chart should display grid lines, otherwise false.
1289 */
1290 protected final boolean isGridVisible() {
1291 return true;
1292 }
1293
1294 protected final void addAnnotationsToRenderer(final XYPlot plot) {
1295
1296 final AnnotationRenderer annotationRenderer = new AnnotationRenderer(getChartSettings(), this.datasets, DEFAULT_FONT_NAME);
1297 annotationRenderer.addAnnotationsToRenderer(plot, this.annotations);
1298
1299 doAddFurtherAnnotations(plot, this.annotations);
1300 }
1301
1302 /**
1303 * Allow further annotation processing, override to implement.
1304 *
1305 * Does nothing by default.
1306 */
1307 protected void doAddFurtherAnnotations(final XYPlot plot, final List<RiverAnnotation> annotations) {
1308
1309 }
77 } 1310 }

http://dive4elements.wald.intevation.org