teichmann@5863: /* Copyright (C) 2011, 2012, 2013 by Bundesanstalt für Gewässerkunde teichmann@5863: * Software engineering by Intevation GmbH teichmann@5863: * teichmann@5994: * This file is Free Software under the GNU AGPL (>=v3) teichmann@5863: * and comes with ABSOLUTELY NO WARRANTY! Check out the teichmann@5994: * documentation coming with Dive4Elements River for details. teichmann@5863: */ teichmann@5863: teichmann@5831: package org.dive4elements.river.exports; ingo@152: ingo@152: import com.lowagie.text.Document; ingo@152: import com.lowagie.text.DocumentException; gernotbelger@9118: import com.lowagie.text.ExceptionConverter; ingo@152: import com.lowagie.text.PageSize; ingo@152: import com.lowagie.text.Rectangle; gernotbelger@9118: import com.lowagie.text.pdf.BaseFont; gernotbelger@9118: import com.lowagie.text.pdf.DefaultFontMapper; gernotbelger@9118: import com.lowagie.text.pdf.DefaultFontMapper.BaseFontParameters; gernotbelger@9118: import com.lowagie.text.pdf.FontMapper; ingo@152: import com.lowagie.text.pdf.PdfContentByte; ingo@152: import com.lowagie.text.pdf.PdfTemplate; ingo@152: import com.lowagie.text.pdf.PdfWriter; ingo@152: gernotbelger@9118: import java.awt.Font; ingo@152: import java.awt.Graphics2D; ingo@152: import java.awt.Transparency; ingo@152: ingo@152: import java.awt.geom.Rectangle2D; gernotbelger@9118: import java.awt.image.BufferedImage; ingo@152: import java.io.IOException; ingo@152: import java.io.OutputStream; ingo@152: import java.io.OutputStreamWriter; ingo@152: import java.io.UnsupportedEncodingException; gernotbelger@9106: import java.text.DateFormat; felix@7046: import java.text.NumberFormat; gernotbelger@9106: import java.util.Date; gernotbelger@9106: import java.util.Locale; rrenkert@7893: import java.util.Map; felix@7046: felix@1036: import org.jfree.chart.ChartRenderingInfo; ingo@152: ingo@152: import javax.imageio.ImageIO; ingo@152: raimund@2231: import au.com.bytecode.opencsv.CSVWriter; raimund@2231: ingo@152: import org.apache.batik.svggen.SVGGraphics2D; ingo@152: import org.apache.batik.svggen.SVGGraphics2DIOException; ingo@152: ingo@152: import org.apache.log4j.Logger; ingo@152: ingo@152: import org.jfree.chart.JFreeChart; raimund@2231: import org.jfree.chart.plot.XYPlot; raimund@2231: import org.jfree.data.xy.XYDataset; gernotbelger@9106: import org.dive4elements.artifacts.Artifact; teichmann@5831: import org.dive4elements.artifacts.CallContext; ingo@152: teichmann@5831: import org.dive4elements.artifacts.common.utils.XMLUtils; gernotbelger@9106: import org.dive4elements.river.FLYS; gernotbelger@9106: import org.dive4elements.river.artifacts.resources.Resources; gernotbelger@9106: import org.dive4elements.river.artifacts.sinfo.util.CalculationUtils; gernotbelger@9586: import org.dive4elements.river.jfree.StripedAreaDataset; rrenkert@7893: import org.dive4elements.river.jfree.XYMetaSeriesCollection; felix@7046: import org.dive4elements.river.utils.Formatter; felix@7046: ingo@152: ingo@152: /** ingo@152: * This class is a helper class which supports some methods to export charts ingo@152: * into specific formats. ingo@152: * ingo@152: * @author Ingo Weinzierl ingo@152: */ ingo@152: public class ChartExportHelper { ingo@152: ingo@1735: public static final String FORMAT_PNG = "png"; ingo@1735: ingo@1735: public static final String FORMAT_PDF = "pdf"; ingo@1735: ingo@1735: public static final String FORMAT_SVG = "svg"; ingo@1735: raimund@2231: public static final String FORMAT_CSV = "csv"; ingo@1735: ingo@152: /** ingo@152: * Constant field to define A4 as default page size. ingo@152: */ ingo@1735: public static final String DEFAULT_PAGE_SIZE = "A4"; ingo@152: ingo@152: /** ingo@152: * Constant field to define UTF-8 as default encoding. ingo@152: */ ingo@1735: public static final String DEFAULT_ENCODING = "UTF-8"; ingo@152: raimund@2231: /** The default separator for the CSV export. */ felix@7045: public static final char DEFAULT_CSV_SEPARATOR = ';'; raimund@2231: raimund@2231: ingo@152: /** ingo@152: * Logger used for logging with log4j. ingo@152: */ ingo@152: private static Logger log = Logger.getLogger(ChartExportHelper.class); ingo@152: ingo@152: ingo@152: /** ingo@152: * A method to export a JFreeChart as image to an ingo@152: * OutputStream with a given format, width and height. ingo@152: * ingo@152: * @param out OutputStream ingo@152: * @param chart JFreeChart object to be exported. felix@3270: * @param cc context, in which e.g. size is stored. ingo@152: * ingo@152: * @throws IOException if writing image to OutputStream failed. ingo@152: */ ingo@152: public static void exportImage( ingo@152: OutputStream out, ingo@1735: JFreeChart chart, ingo@1735: CallContext cc ingo@152: ) ingo@152: throws IOException ingo@152: { ingo@152: log.info("export chart as png"); ingo@152: felix@1036: ChartRenderingInfo info = new ChartRenderingInfo(); felix@1036: ingo@1735: String format = (String) cc.getContextValue("chart.image.format"); ingo@1735: ingo@1735: int[] size = getSize(cc); ingo@1735: ingo@152: ImageIO.write( ingo@152: chart.createBufferedImage( ingo@1735: size[0], size[1], Transparency.BITMASK, info ingo@152: ), ingo@152: format, ingo@152: out ingo@152: ); ingo@152: } ingo@152: ingo@152: ingo@152: /** ingo@152: * A method to export a JFreeChart as SVG to an ingo@152: * OutputStream. ingo@152: * ingo@152: * @param out OutputStream ingo@152: * @param chart JFreeChart to be exported ingo@1735: * @param context The CallContext object that contains extra chart ingo@1735: * parameters. ingo@152: */ ingo@152: public static void exportSVG( ingo@152: OutputStream out, ingo@152: JFreeChart chart, ingo@1735: CallContext context ingo@152: ) { ingo@1735: String encoding = (String) context.getContextValue("chart.encoding"); ingo@1735: ingo@152: log.info("export chart as svg"); ingo@152: ingo@152: if (encoding == null) ingo@152: encoding = DEFAULT_ENCODING; ingo@152: ingo@152: org.w3c.dom.Document document = XMLUtils.newDocument(); ingo@152: SVGGraphics2D graphics = new SVGGraphics2D(document); ingo@152: ingo@1735: int[] size = getSize(context); ingo@1735: felix@2160: ChartRenderingInfo info = new ChartRenderingInfo(); felix@2160: tom@8856: chart.draw( tom@8856: graphics, tom@8856: new Rectangle2D.Double(0.0D, 0.0D,size[0],size[1]), tom@8856: info); ingo@152: ingo@152: try { ingo@152: graphics.stream(new OutputStreamWriter(out, encoding)); ingo@152: } ingo@152: catch (SVGGraphics2DIOException svge) { tom@8856: log.error( tom@8856: "Error while writing svg export to output stream.", svge); ingo@152: } ingo@152: catch (UnsupportedEncodingException uee) { ingo@152: log.error("Unsupported encoding: " + encoding, uee); ingo@152: } ingo@152: } ingo@152: ingo@152: ingo@152: /** ingo@152: * A method to export a JFreeChart as PDF to an ingo@152: * OutputStream. ingo@152: * ingo@152: * @param out OutputStream ingo@152: * @param chart JFreeChart ingo@152: */ ingo@152: public static void exportPDF( ingo@152: OutputStream out, ingo@152: JFreeChart chart, ingo@1735: CallContext cc ingo@152: ) { ingo@152: log.info("export chart as pdf."); ingo@152: ingo@1735: String pageFormat = (String) cc.getContextValue("chart.page.format"); ingo@1735: ingo@152: if (pageFormat == null) ingo@152: pageFormat = DEFAULT_PAGE_SIZE; ingo@152: felix@3270: // Max size of the chart. ingo@152: Rectangle page = PageSize.getRectangle(pageFormat); ingo@152: float pageWidth = page.getWidth(); ingo@152: float pageHeight = page.getHeight(); ingo@152: felix@3270: // The chart width. ingo@1735: int[] size = getSize(cc); ingo@152: ingo@1735: boolean landscape = size[0] > size[1]; ingo@152: ingo@152: float width = 0; ingo@152: float height = 0; ingo@152: if (landscape) { ingo@152: width = pageHeight; ingo@152: height = pageWidth; ingo@152: } ingo@152: else { ingo@152: width = pageWidth; ingo@152: height = pageHeight; ingo@152: } ingo@152: ingo@1735: float marginLeft = (Float) cc.getContextValue( ingo@1735: "chart.marginLeft"); ingo@1735: ingo@1735: float marginRight = (Float) cc.getContextValue( ingo@1735: "chart.marginRight"); ingo@1735: ingo@1735: float marginTop = (Float) cc.getContextValue( ingo@1735: "chart.marginTop"); ingo@1735: ingo@1735: float marginBottom = (Float) cc.getContextValue( ingo@1735: "chart.marginBottom"); ingo@1735: ingo@152: float spaceX = width - marginLeft - marginRight; ingo@1735: if (size[0] > spaceX) { tom@8856: log.warn( tom@8856: "Width of the chart is too big for pdf -> resize it now."); ingo@1735: double ratio = ((double)spaceX) / size[0]; ingo@1735: size[0] *= ratio; ingo@1735: size[1] *= ratio; ingo@1735: log.debug("Resized chart to " + size[0] + "x" + size[1]); ingo@152: } ingo@152: ingo@152: float spaceY = height - marginTop - marginBottom; ingo@1735: if (size[1] > spaceY) { tom@8856: log.warn( tom@8856: "Height of the chart is too big for pdf -> resize it now."); ingo@1735: double ratio = ((double)spaceY) / size[1]; ingo@1735: size[0] *= ratio; ingo@1735: size[1] *= ratio; ingo@1735: log.debug("Resized chart to " + size[0] + "x" + size[1]); ingo@152: } ingo@152: ingo@152: Document document = null; ingo@152: if (landscape) { ingo@152: document = new Document(page.rotate()); ingo@152: log.debug("Create landscape pdf."); aheinecke@7700: } else { aheinecke@7700: document = new Document(page); ingo@152: } ingo@152: ingo@152: try { ingo@152: PdfWriter writer = PdfWriter.getInstance(document, out); ingo@152: tom@8856: document.addSubject( tom@8856: chart.getTitle() != null ? chart.getTitle().getText() : ""); ingo@152: document.addCreationDate(); ingo@152: document.open(); ingo@152: aheinecke@7700: try { aheinecke@7700: PdfContentByte content = writer.getDirectContent(); ingo@152: aheinecke@7700: PdfTemplate template = content.createTemplate(width, height); gernotbelger@9118: gernotbelger@9119: final FontMapper mapper = new ChartExportFontMapper(); gernotbelger@9118: gernotbelger@9118: final Graphics2D graphics = template.createGraphics(width, height ); felix@2160: aheinecke@7700: double[] origin = getCenteredAnchor( aheinecke@7700: marginLeft, marginRight, marginBottom, marginTop, aheinecke@7700: width, height, aheinecke@7700: size[0], size[1]); aheinecke@7700: aheinecke@7700: Rectangle2D area = new Rectangle2D.Double( aheinecke@7700: origin[0], origin[1], size[0], size[1]); aheinecke@7700: aheinecke@7700: ChartRenderingInfo info = new ChartRenderingInfo(); gernotbelger@9118: chart.draw(graphics, area, info); aheinecke@7700: aheinecke@7700: graphics.dispose(); aheinecke@7700: content.addTemplate(template, 0f, 0f); gernotbelger@9118: aheinecke@7700: } aheinecke@7700: finally { aheinecke@7700: document.close(); aheinecke@7700: } aheinecke@7700: } catch (DocumentException de) { ingo@152: log.error("Error while exporting chart to pdf.", de); ingo@152: } ingo@152: } ingo@152: ingo@152: raimund@2231: /** raimund@2231: * A method to export a CSV file to an raimund@2231: * OutputStream. raimund@2231: * raimund@2231: * @param out OutputStream raimund@2231: * @param chart JFreeChart containing the data. raimund@2231: * @param context The CallContext object that contains extra parameters. raimund@2231: */ raimund@2231: public static void exportCSV( raimund@2231: OutputStream out, raimund@2231: JFreeChart chart, raimund@2231: CallContext context) raimund@2231: { raimund@2231: log.debug("export chart as CSV"); gernotbelger@9106: raimund@2231: try { gernotbelger@9189: gernotbelger@9189: // Write BOM so excel is happy gernotbelger@9189: final byte[] BOM = new byte[] {(byte)0xEF, (byte)0xBB, (byte)0xBF}; gernotbelger@9189: out.write(BOM); gernotbelger@9106: gernotbelger@9106: final CSVWriter writer = new CSVWriter( raimund@2231: new OutputStreamWriter( raimund@2231: out, raimund@2231: DEFAULT_ENCODING), aheinecke@7688: DEFAULT_CSV_SEPARATOR, '"', '\\', "\r\n"); gernotbelger@9106: gernotbelger@9106: NumberFormat format = Formatter.getCSVFormatter(context); gernotbelger@9106: gernotbelger@9106: XYPlot plot = chart.getXYPlot(); gernotbelger@9106: int count = plot.getDatasetCount(); gernotbelger@9106: for (int i = 0; i < count; i++) { gernotbelger@9106: XYDataset data = plot.getDataset(i); gernotbelger@9106: int scount = data.getSeriesCount(); gernotbelger@9106: for (int j = 0; j < scount; j++) { gernotbelger@9106: Comparable seriesKey = data.getSeriesKey(j); gernotbelger@9106: log.debug("series key: " + seriesKey.toString()); gernotbelger@9106: Map metaData = null; gernotbelger@9106: if (data instanceof XYMetaSeriesCollection) { gernotbelger@9106: metaData = ((XYMetaSeriesCollection) data).getMetaData(); gernotbelger@9106: } gernotbelger@9586: gernotbelger@9586: if( !(data instanceof StripedAreaDataset) ) { gernotbelger@9586: writeCSVHeader(writer, seriesKey.toString(), metaData); gernotbelger@9586: writeCSVData(writer, data, format); gernotbelger@9586: } gernotbelger@9106: } gernotbelger@9106: } gernotbelger@9106: writer.close(); raimund@2231: } raimund@2231: catch(UnsupportedEncodingException uee) { raimund@2231: log.warn("Wrong encoding for CSV export."); raimund@2231: return; raimund@2231: } raimund@2231: catch(IOException ioe) { raimund@2231: log.error("Writing CSV export failed!"); raimund@2231: } raimund@2231: } raimund@2231: raimund@2231: rrenkert@7893: protected static void writeCSVHeader( rrenkert@7893: CSVWriter writer, rrenkert@7893: String key, rrenkert@7893: Map metaData) rrenkert@7893: { raimund@2231: writer.writeNext(new String[] {"#"}); rrenkert@7893: if (metaData != null) { rrenkert@7893: writer.writeNext(new String[] {"# " + key}); rrenkert@7893: for (Map.Entry entry: metaData.entrySet()) { rrenkert@7893: if (entry.getKey().equals("X") || entry.getKey().equals("Y")) { rrenkert@7893: continue; rrenkert@7893: } rrenkert@7893: writer.writeNext(new String[] rrenkert@7992: {"# " + entry.getKey() + ": " + entry.getValue()}); rrenkert@7893: } rrenkert@7893: writer.writeNext(new String[] {"#"}); rrenkert@7893: writer.writeNext(new String[] { rrenkert@7893: metaData.get("X") != null ? metaData.get("X") : "X", rrenkert@7893: metaData.get("Y") != null ? metaData.get("Y") : "Y"}); rrenkert@7893: } rrenkert@7893: else { rrenkert@7893: writer.writeNext(new String[] {"# " + key}); rrenkert@7893: writer.writeNext(new String[] {"#"}); rrenkert@7893: writer.writeNext(new String[] {"X", "Y"}); rrenkert@7893: } raimund@2231: } raimund@2231: raimund@2231: felix@7047: /** Get x/y data from axis set and write it, on pair per line. */ felix@7047: protected static void writeCSVData( felix@7047: CSVWriter writer, XYDataset data, NumberFormat format) { raimund@2231: int series = data.getSeriesCount(); raimund@2231: for (int i = 0; i < series; i++) { raimund@2231: int items = data.getItemCount(i); aheinecke@7607: double lastX = java.lang.Double.MAX_VALUE; aheinecke@7607: double lastY = java.lang.Double.MAX_VALUE; aheinecke@7607: raimund@2231: for (int j = 0; j < items; j++) { aheinecke@7607: Number x = data.getX(i, j); aheinecke@7607: Number y = data.getY(i, j); aheinecke@7607: double xVal = data.getXValue(i, j); aheinecke@7607: double yVal = data.getYValue(i, j); aheinecke@7607: aheinecke@7607: if (lastX == xVal && lastY == yVal) { aheinecke@7607: // comparing equality is ok here as we want aheinecke@7607: // to find data duplicates like they are added aheinecke@7607: // for example by the StyledSeriesBuilder in aheinecke@7607: // addStepPointsKmQ aheinecke@7607: log.debug("removing duplicate point in series"); aheinecke@7607: continue; aheinecke@7607: } aheinecke@7607: lastX = xVal; aheinecke@7607: lastY = yVal; aheinecke@7607: felix@7094: String xString; felix@7094: String yString; felix@7151: felix@7046: try { aheinecke@7607: xString = java.lang.Double.isNaN(xVal) felix@7151: ? "" aheinecke@7607: : format.format(x); aheinecke@7607: yString = java.lang.Double.isNaN(yVal) felix@7095: ? "" aheinecke@7607: : format.format(y); felix@7046: } felix@7046: catch (NumberFormatException nfe) { aheinecke@7607: xString = x.toString(); aheinecke@7607: yString = y.toString(); felix@7046: } felix@7094: writer.writeNext(new String[] { felix@7094: xString, felix@7094: yString}); raimund@2231: } raimund@2231: } raimund@2231: } raimund@2231: raimund@2231: ingo@1735: public static int[] getSize(CallContext cc) { ingo@1735: int[] size = new int[2]; ingo@1735: ingo@1735: size[0] = (Integer) cc.getContextValue("chart.width"); ingo@1735: size[1] = (Integer) cc.getContextValue("chart.height"); ingo@1735: ingo@1735: return size; ingo@1735: } ingo@1735: ingo@1735: ingo@152: /** tom@8856: * Returns the anchor of the chart so that the chart is centered ingo@152: * according to the given parameters. ingo@152: * ingo@152: * @param mLeft Left margin ingo@152: * @param mRight Right margin ingo@152: * @param mBottom Bottom margin ingo@152: * @param mTop Top margin ingo@152: * @param width The complete width of the drawing area. ingo@152: * @param height The complete height of the drawing area. ingo@152: * @param chartWidth The width of the chart. ingo@152: * @param chartHeight The height of the chart. ingo@152: * ingo@152: * @return an array that contains the anchor for a chart with the given ingo@152: * parameters. The first value is the x point, the second value is the y ingo@152: * point. ingo@152: */ gernotbelger@9106: private static double[] getCenteredAnchor( ingo@152: double mLeft, double mRight, double mBottom, double mTop, ingo@152: double width, double height, ingo@152: double chartWidth, double chartHeight ingo@152: ) { ingo@152: if (log.isDebugEnabled()) { ingo@152: log.debug("Calculate centered origin..."); ingo@152: log.debug("-> PDF width : " + width); ingo@152: log.debug("-> PDF height : " + height); ingo@152: log.debug("-> Chart width : " + chartWidth); ingo@152: log.debug("-> Chart height : " + chartHeight); ingo@152: log.debug("-> margin left : " + mLeft); ingo@152: log.debug("-> margin right : " + mRight); ingo@152: log.debug("-> margin bottom: " + mBottom); ingo@152: log.debug("-> margin top : " + mTop); ingo@152: } ingo@152: ingo@152: double[] origin = new double[2]; ingo@152: ingo@152: double centerX = width / 2; ingo@152: double centerY = height / 2; ingo@152: ingo@152: origin[0] = centerX - chartWidth / 2; ingo@152: origin[1] = centerY - chartHeight / 2; ingo@152: ingo@152: origin[0] = origin[0] >= mLeft ? origin[0] : mLeft; ingo@152: origin[1] = origin[1] >= mTop ? origin[1] : mTop; ingo@152: ingo@152: if (log.isDebugEnabled()) { ingo@152: log.debug("==> centered left origin: " + origin[0]); ingo@152: log.debug("==> centered top origin: " + origin[1]); ingo@152: } ingo@152: ingo@152: return origin; ingo@152: } gernotbelger@9123: }