view artifacts/src/main/java/org/dive4elements/river/exports/WaterlevelExporter.java @ 7597:fca46ce8e4f5

(issue1225) Implement Magic labels. There is now a new value in the chartsettings "Suggested Label" which is hidden in the property editor. A suggested label is the label that combines the label's of all processors that wrote data to an axis. This suggested label is set as the label when the user has not overwritten the label.
author Andre Heinecke <aheinecke@intevation.de>
date Mon, 25 Nov 2013 14:58:14 +0100
parents fe32a7f9655e
children 05549a84ee83
line wrap: on
line source
/* Copyright (C) 2011, 2012, 2013 by Bundesanstalt für Gewässerkunde
 * Software engineering by Intevation GmbH
 *
 * This file is Free Software under the GNU AGPL (>=v3)
 * and comes with ABSOLUTELY NO WARRANTY! Check out the
 * documentation coming with Dive4Elements River for details.
 */

package org.dive4elements.river.exports;

import java.io.IOException;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

import au.com.bytecode.opencsv.CSVWriter;

import org.dive4elements.river.artifacts.model.ConstantWQKms;

import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JRException;

import org.dive4elements.artifacts.Artifact;
import org.dive4elements.artifacts.CallMeta;
import org.dive4elements.artifacts.common.utils.Config;

import org.dive4elements.river.model.Gauge;

import org.dive4elements.river.artifacts.access.FixRealizingAccess;
import org.dive4elements.river.artifacts.access.RangeAccess;
import org.dive4elements.river.artifacts.FixationArtifact;
import org.dive4elements.river.artifacts.D4EArtifact;
import org.dive4elements.river.artifacts.WINFOArtifact;
import org.dive4elements.river.artifacts.StaticWQKmsArtifact;
import org.dive4elements.river.artifacts.model.CalculationResult;
import org.dive4elements.river.artifacts.model.Segment;
import org.dive4elements.river.artifacts.model.WQCKms;
import org.dive4elements.river.artifacts.model.WQKms;
import org.dive4elements.river.artifacts.model.WKmsJRDataSource;
import org.dive4elements.river.artifacts.model.WQKmsResult;
import org.dive4elements.river.artifacts.resources.Resources;

import org.dive4elements.river.utils.RiverUtils;
import org.dive4elements.river.utils.RiverUtils.WQ_MODE;
import org.dive4elements.river.utils.Formatter;

/**
 * Generates different output formats (wst, csv, pdf) of data that resulted from
 * a waterlevel computation.
 *
 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
 */
public class WaterlevelExporter extends AbstractExporter {

    /** The logger used in this exporter.*/
    private static Logger logger = Logger.getLogger(WaterlevelExporter.class);

    public static final String FACET_WST = "wst";

    /* This should be the same as in the StaticWQKmsArtifact */
    public static final String STATICWQKMSNAME = "staticwqkms";

    public static final String CSV_KM_HEADER =
        "export.waterlevel.csv.header.km";

    public static final String CSV_W_HEADER =
        "export.waterlevel.csv.header.w";

    public static final String CSV_Q_HEADER =
        "export.waterlevel.csv.header.q";

    public static final String CSV_Q_DESC_HEADER =
        "export.waterlevel.csv.header.q.desc";

    public static final String CSV_W_DESC_HEADER =
        "export.waterlevel.csv.header.w.desc";

    public static final String CSV_LOCATION_HEADER =
        "export.waterlevel.csv.header.location";

    public static final String CSV_GAUGE_HEADER =
        "export.waterlevel.csv.header.gauge";

    public static final String CSV_META_RESULT =
        "export.waterlevel.csv.meta.result";

    public static final String CSV_META_CREATION =
        "export.waterlevel.csv.meta.creation";

    public static final String CSV_META_CALCULATIONBASE =
        "export.waterlevel.csv.meta.calculationbase";

    public static final String CSV_META_RIVER =
        "export.waterlevel.csv.meta.river";

    public static final String CSV_META_RANGE =
        "export.waterlevel.csv.meta.range";

    public static final String CSV_META_GAUGE =
        "export.waterlevel.csv.meta.gauge";

    public static final String CSV_META_Q =
        "export.waterlevel.csv.meta.q";

    public static final String CSV_META_W =
        "export.waterlevel.csv.meta.w";

    public static final String CSV_NOT_IN_GAUGE_RANGE =
        "export.waterlevel.csv.not.in.gauge.range";

    public static final Pattern NUMBERS_PATTERN =
        Pattern.compile("\\D*(\\d++.\\d*)\\D*");

    public static final String DEFAULT_CSV_KM_HEADER       = "Fluss-Km";
    public static final String DEFAULT_CSV_W_HEADER        = "W [NN + m]";
    public static final String DEFAULT_CSV_Q_HEADER        = "Q [m\u00b3/s]";
    public static final String DEFAULT_CSV_Q_DESC_HEADER   = "Bezeichnung";
    public static final String DEFAULT_CSV_W_DESC_HEADER   = "W/Pegel [cm]";
    public static final String DEFAULT_CSV_LOCATION_HEADER = "Lage";
    public static final String DEFAULT_CSV_GAUGE_HEADER    = "Bezugspegel";
    public static final String DEFAULT_CSV_NOT_IN_GAUGE_RANGE =
        "außerhalb des gewählten Bezugspegels";

    public static final String PDF_HEADER_MODE = "export.waterlevel.pdf.mode";
    public static final String JASPER_FILE     = "export.waterlevel.pdf.file";

    /** The storage that contains all WQKms objects that are calculated.*/
    protected List<WQKms[]> data;

    /** The storage that contains official fixings if available.*/
    protected List<WQKms> officalFixings;

    public WaterlevelExporter() {
        data = new ArrayList<WQKms[]>();
    }

    @Override
    public void generate()
    throws IOException
    {
        logger.debug("WaterlevelExporter.generate");

        /* Check for official fixings. They should also be included in the
         * export but only the calculation result is added with addData */

        officalFixings = new ArrayList<WQKms>();

        for (Artifact art: collection.getArtifactsByName(STATICWQKMSNAME, context)) {
            if (art instanceof StaticWQKmsArtifact) {
                StaticWQKmsArtifact sart = (StaticWQKmsArtifact) art;
                if (!sart.isOfficial()) {
                    continue;
                }

                /* Check that we add the data only once */
                WQKms toAdd = sart.getWQKms();
                String newName = toAdd.getName();

                boolean exists = false;
                for (WQKms wqkm: officalFixings) {
                    /* The same official fixing could be in two
                       artifacts/outs so let's deduplicate */
                    if (wqkm.getName().equals(newName)) {
                        exists = true;
                    }
                }
                if (!exists) {
                    officalFixings.add(toAdd);
                    logger.debug("Adding additional offical fixing: " + newName);
                }
            }
        }

        if (facet != null && facet.equals(AbstractExporter.FACET_CSV)) {
            generateCSV();
        }
        else if (facet != null && facet.equals(FACET_WST)) {
            generateWST();
        }
        else if (facet != null && facet.equals(AbstractExporter.FACET_PDF)) {
            generatePDF();
        }
        else {
            throw new IOException("invalid facet for exporter");
        }
    }


    @Override
    protected void addData(Object d) {
        if (d instanceof CalculationResult) {
            d = ((CalculationResult)d).getData();
            if (d instanceof WQKms []) {
                data.add((WQKms [])d);
            }
            else if (d instanceof WQKmsResult) {
                data.add(((WQKmsResult) d).getWQKms());
            }
        }
    }


    /**
     * Prepare the column titles of waterlevel exports.
     * Titles in this export include the Q value. If a Q value matches a named
     * main value (as HQ100 or MNQ) this named main value should be used as
     * title. This method resets the name of the <i>wqkms</i> object if such
     * named main value fits to the chosen Q.
     *
     * @param winfo A WINFO Artifact.
     * @param wqkms A WQKms object that should be prepared.
     */
    protected String getColumnTitle(WINFOArtifact winfo, WQKms wqkms) {
        logger.debug("WaterlevelExporter.getColumnTitle");

        String name = wqkms.getName();

        logger.debug("Name of WQKms = '" + name + "'");

        if (name.indexOf("W=") >= 0) {
            return name;
        }

        Matcher m = NUMBERS_PATTERN.matcher(name);

        if (m.matches()) {
            String raw = m.group(1);

            try {
                double v = Double.valueOf(raw);

                String nmv = RiverUtils.getNamedMainValue(winfo, v);

                if (nmv != null && nmv.length() > 0) {
                    nmv  = RiverUtils.stripNamedMainValue(nmv);
                    nmv += "=" + String.valueOf(v);
                    logger.debug("Set named main value '" + nmv + "'");

                    return nmv;
                }
            }
            catch (NumberFormatException nfe) {
                // do nothing here
            }
        }

        return name;
    }


    protected String getCSVRowTitle(WINFOArtifact winfo, WQKms wqkms) {
        logger.debug("WaterlevelExporter.prepareNamedValue");

        String name = wqkms.getName();

        logger.debug("Name of WQKms = '" + name + "'");

        WQ_MODE wqmode = RiverUtils.getWQMode(winfo);

        if (wqmode == WQ_MODE.WFREE || wqmode == WQ_MODE.QGAUGE) {
            return localizeWQKms(winfo, wqkms);
        }

        Double v = wqkms.getRawValue();

        String nmv = RiverUtils.getNamedMainValue(winfo, v);

        if (nmv != null && nmv.length() > 0) {
            nmv = RiverUtils.stripNamedMainValue(nmv);
            logger.debug("Set named main value '" + nmv + "'");

            return nmv;
        }

        return localizeWQKms(winfo, wqkms);
    }


    /**
     * Get a string like 'W=' or 'Q=' with a number following in localized
     * format.
     */
    protected String localizeWQKms(WINFOArtifact winfo, WQKms wqkms) {
        WQ_MODE wqmode   = RiverUtils.getWQMode(winfo);
        Double  rawValue = wqkms.getRawValue();

        if (rawValue == null) {
            return wqkms.getName();
        }

        NumberFormat nf = Formatter.getRawFormatter(context);

        if (wqmode == WQ_MODE.WFREE || wqmode == WQ_MODE.WGAUGE) {
            return "W=" + nf.format(rawValue);
        }
        else {
            return "Q=" + nf.format(rawValue);
        }
    }


    @Override
    protected void writeCSVData(CSVWriter writer) {
        logger.info("WaterlevelExporter.writeData");

        WQ_MODE mode    = RiverUtils.getWQMode((D4EArtifact)master);
        boolean atGauge = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.WGAUGE;
        boolean isQ     = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.QFREE;
        RiverUtils.WQ_INPUT input
            = RiverUtils.getWQInputMode((D4EArtifact)master);

        writeCSVMeta(writer);
        writeCSVHeader(writer, atGauge, isQ);

        Double first = Double.NaN;
        Double last = Double.NaN;

        for (WQKms[] tmp: data) {
            for (WQKms wqkms: tmp) {
                wQKms2CSV(writer, wqkms, atGauge, isQ);
                double[] firstLast = wqkms.getFirstLastKM();
                if (first.isNaN()) {
                    /* Initialize */
                    first = firstLast[0];
                    last = firstLast[1];
                }
                if (firstLast[0] > firstLast[1]) {
                    /* Calculating upstream we assert that it is
                     * impossible that the direction changes during this
                     * loop */
                    first = Math.max(first, firstLast[0]);
                    last = Math.min(last, firstLast[1]);
                } else if (firstLast[0] < firstLast[1]) {
                    first = Math.min(first, firstLast[0]);
                    last = Math.max(last, firstLast[1]);
                } else {
                    first = last = firstLast[0];
                }
            }
        }
        /* Append the official fixing at the bottom */
        for (WQKms wqkms: officalFixings) {
            wQKms2CSV(writer, filterWQKms(wqkms, first, last), atGauge, isQ);
        }
    }


    /** Filter a wqkms object to a distance.
     *
     * To handle upstream / downstream and to limit
     * the officialFixings to the calculation distance
     * we create a new wqkms object here and fill it only
     * with the relevant data.
     *
     * @param wqkms: The WQKms Object to filter
     * @param first: The fist kilometer of the range
     * @param last: The last kilometer of the range
     *
     * @return A new WQKms with the relevant data sorted by direction
     */
    private WQKms filterWQKms(WQKms wqkms, Double first, Double last) {
        if (first.isNaN() || last.isNaN()) {
            logger.warn("Filtering official fixing without valid first/last.");
            return wqkms;
        }
        int firstIdx = first > last ? wqkms.size() - 1 : 0;
        int lastIdx  = first > last ? 0 : wqkms.size() -1;
        WQKms filtered = new WQKms (wqkms.size());
        filtered.setReferenceSystem(wqkms.getReferenceSystem());
        filtered.setName(wqkms.getName());
        double [] dp = new double [3];

        if (first > last) {
            for (int i = wqkms.size() - 1; i >= 0; i--) {
                dp = wqkms.get(i, dp);
                if (dp[2] <= first + 1E-5 && dp[2] > last - 1E-5) {
                    filtered.add(dp[0], dp[1], dp[2]);
                }
            }
        } else {
            for (int i = 0, N = wqkms.size(); i < N; i++) {
                dp = wqkms.get(i, dp);
                if (dp[2] < last + 1E-5 && dp[2] > first - 1E-5) {
                    filtered.add(dp[0], dp[1], dp[2]);
                }
            }
        }
        return filtered;
    }


    protected void writeCSVMeta(CSVWriter writer) {
        logger.info("WaterlevelExporter.writeCSVMeta");

        // TODO use Access instead of RiverUtils

        CallMeta meta = context.getMeta();

        D4EArtifact flys = (D4EArtifact) master;

        writer.writeNext(new String[] {
            Resources.getMsg(
                meta,
                CSV_META_RESULT,
                CSV_META_RESULT,
                new Object[] { RiverUtils.getRivername(flys) })
        });

        Locale locale = Resources.getLocale(meta);
        DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);

        writer.writeNext(new String[] {
            Resources.getMsg(
                meta,
                CSV_META_CREATION,
                CSV_META_CREATION,
                new Object[] { df.format(new Date()) })
        });

        writer.writeNext(new String[] {
            Resources.getMsg(
                meta,
                CSV_META_CALCULATIONBASE,
                CSV_META_CALCULATIONBASE,
                new Object[] { "" }) // TODO what is required at this place?
        });

        writer.writeNext(new String[] {
            Resources.getMsg(
                meta,
                CSV_META_RIVER,
                CSV_META_RIVER,
                new Object[] { RiverUtils.getRivername(flys) })
        });

        RangeAccess rangeAccess = new RangeAccess(flys);
        double[] kms = rangeAccess.getKmRange();
        writer.writeNext(new String[] {
            Resources.getMsg(
                meta,
                CSV_META_RANGE,
                CSV_META_RANGE,
                new Object[] { kms[0], kms[kms.length-1] })
        });

        writer.writeNext(new String[] {
            Resources.getMsg(
                meta,
                CSV_META_GAUGE,
                CSV_META_GAUGE,
                new Object[] { RiverUtils.getGaugename(flys) })
        });

        RiverUtils.WQ_MODE wq = RiverUtils.getWQMode(flys);
        if (wq == RiverUtils.WQ_MODE.QFREE || wq == RiverUtils.WQ_MODE.QGAUGE) {
            double[] qs  = RiverUtils.getQs(flys);
            RiverUtils.WQ_INPUT input = RiverUtils.getWQInputMode(flys);

            String data = "";

            if ((input == RiverUtils.WQ_INPUT.ADAPTED ||
                input == RiverUtils.WQ_INPUT.RANGE) &&
                qs != null && qs.length > 0)
            {
                data = String.valueOf(qs[0]);
                data += " - " + String.valueOf(qs[qs.length-1]);
            }
            else if (input == RiverUtils.WQ_INPUT.SINGLE && qs != null){
                data = String.valueOf(qs[0]);
                for (int i = 1; i < qs.length; i++) {
                    data += ", " + String.valueOf(qs[i]);
                }
            }
            else {
                logger.warn("Could not determine Q range!");
            }

            writer.writeNext(new String[] {
                Resources.getMsg(
                    meta,
                    CSV_META_Q,
                    CSV_META_Q,
                    new Object[] {data})
            });
        }
        else {
            double[] ws = RiverUtils.getWs(flys);

            String lower = "";
            String upper = "";

            if (ws != null && ws.length > 0) {
                lower = String.valueOf(ws[0]);
                upper = String.valueOf(ws[ws.length-1]);
            }
            else {
                logger.warn("Could not determine W range!");
            }

            writer.writeNext(new String[] {
                Resources.getMsg(
                    meta,
                    CSV_META_W,
                    CSV_META_W,
                    new Object[] { lower, upper })
            });
        }

        writer.writeNext(new String[] { "" });
    }


    /**
     * Write the header, with different headings depending on whether at a
     * gauge or at a location.
     */
    protected void writeCSVHeader(
        CSVWriter writer,
        boolean   atGauge,
        boolean   isQ
    ) {
        logger.info("WaterlevelExporter.writeCSVHeader");

        String unit = RiverUtils.getRiver((D4EArtifact) master).getWstUnit().getName();

        if (atGauge) {
            writer.writeNext(new String[] {
                msg(CSV_KM_HEADER, DEFAULT_CSV_KM_HEADER),
                msg(CSV_W_HEADER, DEFAULT_CSV_W_HEADER, new Object[] { unit }),
                msg(CSV_Q_HEADER, DEFAULT_CSV_Q_HEADER),
                (isQ
                    ? msg(CSV_Q_DESC_HEADER, DEFAULT_CSV_Q_DESC_HEADER)
                    : msg(CSV_W_DESC_HEADER, DEFAULT_CSV_W_DESC_HEADER)),
                msg(CSV_LOCATION_HEADER, DEFAULT_CSV_LOCATION_HEADER),
                msg(CSV_GAUGE_HEADER, DEFAULT_CSV_GAUGE_HEADER)
            });
        }
        else {
            writer.writeNext(new String[] {
                msg(CSV_KM_HEADER, DEFAULT_CSV_KM_HEADER),
                    // TODO flys/issue1128 (unit per river)
                msg(CSV_W_HEADER, DEFAULT_CSV_W_HEADER, new Object[] { unit }),
                msg(CSV_Q_HEADER, DEFAULT_CSV_Q_HEADER),
                msg(CSV_LOCATION_HEADER, DEFAULT_CSV_LOCATION_HEADER)
            });
        }
    }


    /** Linearly search for gauge which is valid at km. */
    private static Gauge findGauge(double km, List<Gauge> gauges) {
        for (Gauge gauge: gauges) {
            if (gauge.getRange().contains(km)) {
                return gauge;
            }
        }
        return null;
    }

    private static Segment findSegment(double km, List<Segment> segments) {
        for (Segment segment: segments) {
            if (segment.inside(km)) {
                return segment;
            }
        }
        return null;
    }


    private void writeRow4(CSVWriter writer, double wqkm[], D4EArtifact flys) {
        NumberFormat kmf = getKmFormatter();
        NumberFormat wf  = getWFormatter();
        NumberFormat qf  = getQFormatter();
        writer.writeNext(new String[] {
            kmf.format(wqkm[2]),
            wf.format(wqkm[0]),
            qf.format(RiverUtils.roundQ(wqkm[1])),
            RiverUtils.getLocationDescription(flys, wqkm[2])
        });
    }

    /** Write an csv-row at gauge location. */
    private void writeRow6(CSVWriter writer, double wqkm[], String wOrQDesc,
        D4EArtifact flys, String gaugeName) {
        NumberFormat kmf = getKmFormatter();
        NumberFormat wf  = getWFormatter();
        NumberFormat qf  = getQFormatter();

        writer.writeNext(new String[] {
            kmf.format(wqkm[2]),
            wf.format(wqkm[0]),
            qf.format(RiverUtils.roundQ(wqkm[1])),
            wOrQDesc,
            RiverUtils.getLocationDescription(flys, wqkm[2]),
            gaugeName
        });
    }

    private String getDesc(WQKms wqkms, boolean isQ)
    {
        D4EArtifact flys = (D4EArtifact) master;
        String colDesc = "";

        if (flys instanceof WINFOArtifact && isQ) {
            colDesc = getCSVRowTitle((WINFOArtifact)flys, wqkms);
        }
        else if (!isQ) {
            Double value = RiverUtils.getValueFromWQ(wqkms);
            colDesc = (value != null) ?
                Formatter.getWaterlevelW(context).format(value) : null;
        }

        if (flys instanceof WINFOArtifact) {
            if (wqkms != null && wqkms.getRawValue() != null) {
                WINFOArtifact winfo = (WINFOArtifact) flys;
                colDesc = RiverUtils.getNamedMainValue(winfo, wqkms.getRawValue());
                // For 'W am Pegel' s
                if (colDesc == null) {
                    Double value = RiverUtils.getValueFromWQ(wqkms);
                    colDesc = (value != null) ?
                        Formatter.getWaterlevelW(context).format(value) : null;
                }
            }
        }
        if (colDesc != null) {
            /* Quick hack. Can be removed when database strings are
             * adapted or left in here as it should never be harmful. */
            colDesc = colDesc.replace("Amtl.Festlegung_", "Amtl. ");
        }

        return colDesc == null ? "" : colDesc;
    }

    /**
     * Write "rows" of csv data from wqkms with writer.
     */
    protected void wQKms2CSV(
        CSVWriter writer,
        WQKms     wqkms,
        boolean   atGauge,
        boolean   isQ
    ) {
        logger.debug("WaterlevelExporter.wQKms2CSV");

        // Skip constant data.
        if (wqkms instanceof ConstantWQKms) {
            return;
        }

        NumberFormat kmf = getKmFormatter();
        NumberFormat wf  = getWFormatter();
        NumberFormat qf  = getQFormatter();

        int      size   = wqkms.size();
        double[] result = new double[3];

        D4EArtifact flys       = (D4EArtifact) master;
        List<Gauge>  gauges     = RiverUtils.getGauges(flys);
        Gauge        gauge      = RiverUtils.getGauge(flys);
        String       gaugeName  = gauge.getName();
        String       desc       = "";
        String       notinrange = msg(
            CSV_NOT_IN_GAUGE_RANGE,
            DEFAULT_CSV_NOT_IN_GAUGE_RANGE);
        List<Segment> segments = null;
        boolean isFixRealize = false;

        double a = gauge.getRange().getA().doubleValue();
        double b = gauge.getRange().getB().doubleValue();
        long startTime = System.currentTimeMillis();

        desc = getDesc(wqkms, isQ);

        if (flys instanceof FixationArtifact) {
            // Get W/Q input per gauge for this case.
            FixRealizingAccess fixAccess = new FixRealizingAccess(flys);
            segments = fixAccess.getSegments();
            if (segments != null && !segments.isEmpty()) {
                isFixRealize = true;
            }
        }

        if (atGauge) { // "At gauge" needs more output.

            // Kms tend to be close together so caching the last sector
            // is a good time saving heuristic.
            Segment lastSegment = null;
            Gauge   lastGauge   = null;

            NumberFormat nf =
                Formatter.getFormatter(context.getMeta(), 0, 0);

            for (int i = 0; i < size; ++i) {
                result = wqkms.get(i, result);
                double km = result[2];

                if (segments != null) {
                    Segment found = lastSegment != null
                                    && lastSegment.inside(km)
                        ? lastSegment
                        : findSegment(km, segments);

                    if (found != null) {
                        desc = nf.format(found.getValues()[0]);
                    }
                    lastSegment = found;
                }

                String gaugeN;
                if (isFixRealize) {
                    Gauge found = lastGauge != null
                                  && lastGauge.getRange().contains(km)
                        ? lastGauge
                        : findGauge(km, gauges);

                    gaugeN = found != null ? found.getName() : notinrange;
                    lastGauge = found;
                }
                else {
                    // TODO issue1114: Take correct gauge
                    gaugeN = km >= a && km <= b
                        ? gaugeName
                        : notinrange;
                }
                writeRow6(writer, result, desc, flys, gaugeN);
            }
        }
        else { // Not at gauge.
            for (int i = 0; i < size; ++i) {
                result = wqkms.get(i, result);
                writeRow4(writer, result, flys);
            }
        }

        long stopTime = System.currentTimeMillis();

        if (logger.isDebugEnabled()) {
            logger.debug("Writing CSV took " +
                (float)(stopTime-startTime)/1000f + " secs.");
        }
    }


    /**
     * Generates the output in WST format.
     */
    protected void generateWST()
    throws    IOException
    {
        logger.info("WaterlevelExporter.generateWST");

        int cols = data.get(0).length;
        WstWriter writer = new WstWriter(cols);

        writeWSTData(writer);

        writer.write(out);
    }


    protected void writeWSTData(WstWriter writer) {
        logger.debug("WaterlevelExporter.writeWSTData");

        double[] result = new double[4];

        for (WQKms[] tmp: data) {
            for (WQKms wqkms: tmp) {
                if (wqkms instanceof ConstantWQKms) {
                    continue;
                }
                int size = wqkms != null ? wqkms.size() : 0;

                addWSTColumn(writer, wqkms);

                for (int i = 0; i < size; i++) {
                    result = wqkms.get(i, result);

                    writer.add(result);
                }

                if (wqkms instanceof WQCKms) {
                    addWSTColumn(writer, wqkms);

                    for (int c = 0; c < size; c++) {
                        result = wqkms.get(c, result);

                        writer.addCorrected(result);
                    }
                }
            }
        }
    }


    /**
     * Register a new column at <i>writer</i>. The name /
     * title of the column depends on the Q or W value of <i>wqkms</i>. If a Q
     * was selected and the Q fits to a named main value, the title is set to
     * the named main value. Otherwise, the name returned by
     * <i>WQKms.getName()</i> is set.
     *
     * @param writer The WstWriter.
     * @param wqkms The new WST column.
     */
    protected void addWSTColumn(WstWriter writer, WQKms wqkms) {
        if (wqkms instanceof ConstantWQKms) {
            return;
        }
        if (master instanceof WINFOArtifact) {
            writer.addColumn(getColumnTitle((WINFOArtifact) master, wqkms));
        }
        else {
            writer.addColumn(wqkms.getName());
        }
    }


    @Override
    protected void writePDF(OutputStream out) {
        logger.debug("write PDF");
        WKmsJRDataSource source = createJRData();

        String jasperFile = Resources.getMsg(
                                context.getMeta(),
                                JASPER_FILE,
                                "/jasper/waterlevel_en.jasper");
        String confPath = Config.getConfigDirectory().toString();


        Map parameters = new HashMap();
        parameters.put("ReportTitle", "Exported Data");
        try {
            JasperPrint print = JasperFillManager.fillReport(
                confPath + jasperFile,
                parameters,
                source);
            JasperExportManager.exportReportToPdfStream(print, out);
        }
        catch(JRException je) {
            logger.warn("Error generating PDF Report!", je);
        }
    }

    protected WKmsJRDataSource createJRData() {
        WKmsJRDataSource source = new WKmsJRDataSource();

        WQ_MODE mode    = RiverUtils.getWQMode((D4EArtifact)master);
        boolean atGauge = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.WGAUGE;
        boolean isQ     = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.QFREE;

        Double first = Double.NaN;
        Double last = Double.NaN;

        addMetaData(source);
        for (WQKms[] tmp: data) {
            for (WQKms wqkms: tmp) {
                addWKmsData(wqkms, atGauge, isQ, source);
                double[] firstLast = wqkms.getFirstLastKM();
                if (first.isNaN()) {
                    /* Initialize */
                    first = firstLast[0];
                    last = firstLast[1];
                }
                if (firstLast[0] > firstLast[1]) {
                    /* Calculating upstream we assert that it is
                     * impossible that the direction changes during this
                     * loop */
                    first = Math.max(first, firstLast[0]);
                    last = Math.min(last, firstLast[1]);
                } else if (firstLast[0] < firstLast[1]) {
                    first = Math.min(first, firstLast[0]);
                    last = Math.max(last, firstLast[1]);
                } else {
                    first = last = firstLast[0];
                }
            }
        }

        /* Append the official fixing at the bottom */
        for (WQKms wqkms: officalFixings) {
            addWKmsData(filterWQKms(wqkms, first, last), atGauge, isQ, source);
        }
        return source;
    }

    protected void addMetaData(WKmsJRDataSource source) {
        CallMeta meta = context.getMeta();

        D4EArtifact flys = (D4EArtifact) master;

        source.addMetaData ("river", RiverUtils.getRivername(flys));

        Locale locale = Resources.getLocale(meta);
        DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
        NumberFormat kmf = getKmFormatter();

        source.addMetaData("date", df.format(new Date()));

        RangeAccess rangeAccess = new RangeAccess(flys);
        double[] kms = rangeAccess.getKmRange();
        source.addMetaData("range",
                kmf.format(kms[0]) + " - " + kmf.format(kms[kms.length-1]));

        source.addMetaData("gauge", RiverUtils.getGaugename(flys));

        source.addMetaData("calculation", Resources.getMsg(
                                            locale,
                                            PDF_HEADER_MODE,
                                            "Waterlevel"));
    }

    protected void addWKmsData(
        WQKms wqkms,
        boolean atGauge,
        boolean isQ,
        WKmsJRDataSource source)
    {
        logger.debug("WaterlevelExporter.addWKmsData");

        // Skip constant data.
        if (wqkms instanceof ConstantWQKms) {
            return;
        }

        NumberFormat kmf = getKmFormatter();
        NumberFormat wf  = getWFormatter();
        NumberFormat qf  = getQFormatter();

        int      size   = wqkms.size();
        double[] result = new double[3];

        D4EArtifact flys       = (D4EArtifact) master;
        Gauge        gauge      = RiverUtils.getGauge(flys);
        String       gaugeName  = gauge.getName();
        String       desc       = "";
        String       notinrange = msg(
            CSV_NOT_IN_GAUGE_RANGE,
            DEFAULT_CSV_NOT_IN_GAUGE_RANGE);

        double a = gauge.getRange().getA().doubleValue();
        double b = gauge.getRange().getB().doubleValue();

        desc = getDesc(wqkms, isQ);
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < size; i ++) {
            result = wqkms.get(i, result);

            if (atGauge) {
                source.addData(new String[] {
                    kmf.format(result[2]),
                    wf.format(result[0]),
                    qf.format(RiverUtils.roundQ(result[1])),
                    desc,
                    RiverUtils.getLocationDescription(flys, result[2]),
                    result[2] >= a && result[2] <= b
                        ? gaugeName
                        : notinrange
                });
            }
            else {
                source.addData(new String[] {
                    kmf.format(result[2]),
                    wf.format(result[0]),
                    qf.format(RiverUtils.roundQ(result[1])),
                    desc,
                    RiverUtils.getLocationDescription(flys, result[2]),
                    result[2] >= a && result[2] <= b
                        ? gaugeName
                        : notinrange
                });
            }
        }

        long stopTime = System.currentTimeMillis();

        if (logger.isDebugEnabled()) {
            logger.debug("Writing PDF data took " +
                (float)(stopTime-startTime)/1000f + " secs.");
        }
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org