view artifacts/src/main/java/org/dive4elements/river/exports/WaterlevelExporter.java @ 9638:6c1ebf2220f5

# 19b (check for usages of WaterlevelExporter.getWforGaugeAndQ) -> cleanup: removing calc.extreme.curve
author dnt_bjoernsen <d.tironi@bjoernsen.de>
date Thu, 31 Oct 2019 17:37:53 +0100
parents 2b1626fa4a95
children
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.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;
import org.dive4elements.artifacts.Artifact;
import org.dive4elements.artifacts.CallMeta;
import org.dive4elements.river.artifacts.AbstractFixBunduArtifact;
import org.dive4elements.river.artifacts.D4EArtifact;
import org.dive4elements.river.artifacts.StaticWQKmsArtifact;
import org.dive4elements.river.artifacts.WINFOArtifact;
import org.dive4elements.river.artifacts.access.FixRealizingAccess;
import org.dive4elements.river.artifacts.access.IsOfficialAccess;
import org.dive4elements.river.artifacts.access.RangeAccess;
import org.dive4elements.river.artifacts.common.DefaultCalculationResults;
import org.dive4elements.river.artifacts.common.ExportContextPDF;
import org.dive4elements.river.artifacts.common.GeneralResultType;
import org.dive4elements.river.artifacts.common.JasperDesigner;
import org.dive4elements.river.artifacts.common.JasperReporter;
import org.dive4elements.river.artifacts.common.MetaAndTableJRDataSource;
import org.dive4elements.river.artifacts.model.CalculationResult;
import org.dive4elements.river.artifacts.model.ConstantWQKms;
import org.dive4elements.river.artifacts.model.DischargeTables;
import org.dive4elements.river.artifacts.model.Segment;
import org.dive4elements.river.artifacts.model.WKmsJRDataSource;
import org.dive4elements.river.artifacts.model.WQCKms;
import org.dive4elements.river.artifacts.model.WQKms;
import org.dive4elements.river.artifacts.model.WQKmsResult;
import org.dive4elements.river.artifacts.model.WstLine;
import org.dive4elements.river.artifacts.resources.Resources;
import org.dive4elements.river.artifacts.sinfo.util.CalculationUtils;
import org.dive4elements.river.artifacts.sinfo.util.RiverInfo;
import org.dive4elements.river.model.DischargeTable;
import org.dive4elements.river.model.Gauge;
import org.dive4elements.river.utils.Formatter;
import org.dive4elements.river.utils.RiverUtils;
import org.dive4elements.river.utils.RiverUtils.WQ_MODE;

import au.com.bytecode.opencsv.CSVWriter;
import gnu.trove.TDoubleArrayList;
import net.sf.jasperreports.engine.JRException;

/**
 * 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 log used in this exporter. */
    private static Logger log = Logger.getLogger(WaterlevelExporter.class);

    private static final String FACET_WST = "wst";

    /* This should be the same as in the StaticWQKmsArtifact */
    private 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 = "common.export.csv.header.q";

    /**
     * @deprecated Use {@link WaterlevelDescriptionBuilder} instead.
     */
    @Deprecated
    public static final String CSV_Q_DESC_HEADER = "export.waterlevel.csv.header.q.desc";

    /**
     * @deprecated Use {@link WaterlevelDescriptionBuilder} instead.
     */
    @Deprecated
    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";

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

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

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

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

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

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

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

    private 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";

    private 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]";
    /**
     * @deprecated Use {@link WaterlevelDescriptionBuilder} instead.
     */
    @Deprecated
    public static final String DEFAULT_CSV_Q_DESC_HEADER = "Bezeichnung";
    /**
     * @deprecated Use {@link WaterlevelDescriptionBuilder} instead.
     */
    @Deprecated
    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";

    protected static final String PDF_HEADER_MODE = "export.waterlevel.pdf.mode";

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

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

    private final Map<String, Double> gaugeQ_W_Map = new HashMap<>();

    public WaterlevelExporter() {
        this.data = new ArrayList<>();
    }

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

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

        this.officalFixings = new ArrayList<>();

        for (final Artifact art : this.collection.getArtifactsByName(STATICWQKMSNAME, this.context)) {
            if (art instanceof StaticWQKmsArtifact) {
                final IsOfficialAccess access = new IsOfficialAccess((D4EArtifact) art);
                final StaticWQKmsArtifact sart = (StaticWQKmsArtifact) art;
                if (!access.isOfficial()) {
                    continue;
                }

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

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

        if (this.facet != null && this.facet.equals(AbstractExporter.FACET_CSV)) {
            generateCSV();
        } else if (this.facet != null && this.facet.equals(FACET_WST)) {
            generateWST();
        } else if (this.facet != null && this.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[]) {
                this.data.add((WQKms[]) d);
            } else if (d instanceof WQKmsResult) {
                this.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.
     */
    public String getColumnTitle(final WINFOArtifact winfo, final WQKms wqkms) {
        log.debug("WaterlevelExporter.getColumnTitle");

        final String name = wqkms.getName();

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

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

        final Matcher m = NUMBERS_PATTERN.matcher(name);

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

            try {
                final 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);
                    log.debug("Set named main value '" + nmv + "'");

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

        return name;
    }

    /**
     * @deprecated Use {@link WaterlevelDescriptionBuilder} instead.
     */
    @Deprecated
    public String getCSVRowTitle(final WINFOArtifact winfo, final WQKms wqkms) {
        log.debug("WaterlevelExporter.prepareNamedValue");

        final String name = wqkms.getName();

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

        final WQ_MODE wqmode = RiverUtils.getWQMode(winfo);

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

        final Double v = wqkms.getRawValue();

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

        if (nmv != null && nmv.length() > 0) {
            nmv = RiverUtils.stripNamedMainValue(nmv);
            log.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.
     *
     * @deprecated Use {@link WaterlevelDescriptionBuilder} instead.
     */
    @Deprecated
    public String localizeWQKms(final WINFOArtifact winfo, final WQKms wqkms) {
        final WQ_MODE wqmode = RiverUtils.getWQMode(winfo);
        final Double rawValue = wqkms.getRawValue();

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

        final NumberFormat nf = Formatter.getRawFormatter(this.context);

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

    private final boolean isQ() {
        final WQ_MODE mode = RiverUtils.getWQMode((D4EArtifact) this.master);
        return mode == WQ_MODE.QGAUGE || mode == WQ_MODE.QFREE;
    }

    @Override
    public void writeCSVData(final CSVWriter writer) {
        log.info("WaterlevelExporter.writeData");
        final WQ_MODE mode = RiverUtils.getWQMode((D4EArtifact) this.master);
        final boolean atGauge = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.WGAUGE;
        final boolean isQ = isQ();

        final RiverUtils.WQ_INPUT input = RiverUtils.getWQInputMode((D4EArtifact) this.master);
        writeCSVMeta(writer);
        writeCSVHeader(writer, atGauge, isQ);

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

        for (final WQKms[] tmp : this.data) {
            for (final WQKms wqkms : tmp) {
                wQKms2CSV(writer, wqkms, atGauge, isQ);
                final 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 (final WQKms wqkms : this.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
     */
    public final WQKms filterWQKms(final WQKms wqkms, final Double first, final Double last) {
        if (first.isNaN() || last.isNaN()) {
            log.warn("Filtering official fixing without valid first/last.");
            return wqkms;
        }
        final int firstIdx = first > last ? wqkms.size() - 1 : 0;
        final int lastIdx = first > last ? 0 : wqkms.size() - 1;
        final WQKms filtered = new WQKms(wqkms.size());
        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;
    }

    public void writeCSVMeta(final CSVWriter writer) {
        log.info("WaterlevelExporter.writeCSVMeta");

        // TODO use Access instead of RiverUtils

        final CallMeta meta = this.context.getMeta();

        final D4EArtifact flys = (D4EArtifact) this.master;

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

        final Locale locale = Resources.getLocale(meta);
        final 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) }) });

        final RangeAccess rangeAccess = new RangeAccess(flys);
        final 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] }) });

        final String gaugeName = RiverUtils.getGaugename(flys);
        writer.writeNext(new String[] {
                Resources.getMsg(meta, CSV_META_GAUGE, CSV_META_GAUGE, new Object[] { gaugeName != null ? gaugeName : Resources.getMsg(meta, "-") }) });

        // TODO: code extracted into WaterlevelDescriptionBuilder, should be used instead.
        final RiverUtils.WQ_MODE wq = RiverUtils.getWQMode(flys);
        if (wq == RiverUtils.WQ_MODE.QFREE || wq == RiverUtils.WQ_MODE.QGAUGE) {
            final double[] qs = RiverUtils.getQs(flys);
            final 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 {
                log.warn("Could not determine Q range!");
            }

            writer.writeNext(new String[] { Resources.getMsg(meta, CSV_META_Q, CSV_META_Q, new Object[] { data }) });
        } else {
            final 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 {
                log.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(final CSVWriter writer, final boolean atGauge, final boolean isQ) {
        log.info("WaterlevelExporter.writeCSVHeader");

        final String unit = RiverUtils.getRiver((D4EArtifact) this.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),

                    // FIXME: use WaterlevelDescriptionBuilder instead and also remove all this duplicate code.
                    (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(final double km, final List<Gauge> gauges) {
        for (final Gauge gauge : gauges) {
            if (gauge.getRange().contains(km)) {
                return gauge;
            }
        }
        return null;
    }

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

    protected void writeRow4(final CSVWriter writer, final double wqkm[], final D4EArtifact flys, final Gauge gauge, final boolean isQ) {
        final NumberFormat kmf = getKmFormatter();
        final NumberFormat wf = getWFormatter();
        final 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. */
    protected void writeRow6(final CSVWriter writer, final double wqkm[], final String wOrQDesc, final D4EArtifact flys, final String gaugeName,
            final String wAtGauge, final boolean isQ) {
        final NumberFormat kmf = getKmFormatter();
        final NumberFormat wf = getWFormatter();
        final 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 });
    }

    /**
     * @deprecated Use {@link WaterlevelDescriptionBuilder} instead.
     */
    @Deprecated
    public final String getDesc(final WQKms wqkms, final boolean isQ) {
        final D4EArtifact flys = (D4EArtifact) this.master;
        String colDesc = "";

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

        if (flys instanceof WINFOArtifact) {
            if (wqkms != null && wqkms.getRawValue() != null) {
                final WINFOArtifact winfo = (WINFOArtifact) flys;
                colDesc = RiverUtils.getNamedMainValue(winfo, wqkms.getRawValue());
                // For 'W am Pegel' s
                if (colDesc == null) {
                    final Double value = RiverUtils.getValueFromWQ(wqkms);
                    colDesc = (value != null) ? Formatter.getWaterlevelW(this.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;
    }

    private List<Segment> getSegments(final D4EArtifact flys) {
        if (flys instanceof AbstractFixBunduArtifact) {
            // Get W/Q input per gauge for this case.
            final FixRealizingAccess fixAccess = new FixRealizingAccess(flys);
            return fixAccess.getSegments();

        }
        return null;
    }

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

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

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

        final D4EArtifact flys = (D4EArtifact) this.master;
        final RangeAccess rangeAccess = new RangeAccess(flys);

        final List<Gauge> gauges = RiverUtils.getGauges(flys);

        Gauge gauge = rangeAccess.getRiver().determineRefGauge(rangeAccess.getKmRange(), rangeAccess.isRange());

        // REMARK gauge may be null when rangeAccess starts outside any gauge range
        if (gauge == null)
            gauge = rangeAccess.getRiver().determineRefGauge(wqkms.getKms(), rangeAccess.isRange());

        final String notinrange = msg(CSV_NOT_IN_GAUGE_RANGE, DEFAULT_CSV_NOT_IN_GAUGE_RANGE);
        final String gaugeName = gauge != null ? gauge.getName() : notinrange;

        final double a = gauge != null ? gauge.getRange().getA().doubleValue() : Double.NaN;
        final double b = gauge != null ? gauge.getRange().getB().doubleValue() : Double.NaN;

        final long startTime = System.currentTimeMillis();

        String desc = getDesc(wqkms, isQ);

        final List<Segment> segments = getSegments(flys);
        final boolean isFixRealize = isFixrealize(segments);
        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;

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

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

                final double station = result[2];
                final double q = result[1];

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

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

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

                    gaugeN = found != null ? found.getName() : notinrange;
                    lastGauge = found;

                    wAtGauge = this.getWaterlevel(q, found); // THIS IS NEW (and makes common super method difficult)
                } else {
                    // TODO issue1114: Take correct gauge
                    gaugeN = station >= a && station <= b ? gaugeName : notinrange;

                    wAtGauge = "";
                }

                writeRow6(writer, result, desc, flys, gaugeN, wAtGauge, isQ);
            }
        } else { // Not at gauge.
            for (int i = 0; i < size; ++i) {
                result = wqkms.get(i, result);
                writeRow4(writer, result, flys, gauge, isQ);
            }
        }

        final long stopTime = System.currentTimeMillis();

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

    private boolean isFixrealize(final List<Segment> segments) {
        boolean isFixRealize = false;
        if (segments != null && !segments.isEmpty()) {
            isFixRealize = true;
        }
        return isFixRealize;
    }

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

        final int cols = this.data.get(0).length + this.officalFixings.size();
        final WstWriter writer = new WstWriter(cols);

        writeWSTData(writer);

        writer.write(this.out);
    }

    public void writeWSTData(final WstWriter writer) {
        log.debug("WaterlevelExporter.writeWSTData");

        double[] result = new double[4];

        for (final WQKms[] tmp : this.data) {
            for (final WQKms wqkms : tmp) {
                if (wqkms instanceof ConstantWQKms) {
                    continue;
                }
                final 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);
                    }
                }
            }
        }

        // Append the official fixing interpolated to the calculation steps
        //
        // There was some confusion how to implement this. see flys/issue1620
        // for details.
        for (final WQKms wqkms : this.officalFixings) {
            // To add some spaces here or to add them in the writer,..
            writer.addColumn(getDesc(wqkms, true));

            // Get all lines from the calculation
            final Map<Double, WstLine> calcLines = writer.getLines();

            // All KM values where we have a point for
            final TDoubleArrayList officialKms = wqkms.allKms();

            for (final Map.Entry<Double, WstLine> entry : calcLines.entrySet()) {
                // Bad for perfomance but the user can wait a bit for WST
                // so lets not spend time optimizing too much,.. *hides*
                final double km = entry.getKey().doubleValue();
                final int idx = officialKms.indexOf(km);
                if (idx != -1) {
                    entry.getValue().add(wqkms.getW(idx), wqkms.getQ(idx));
                }
            }
        }
    }

    /**
     * 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(final WstWriter writer, final WQKms wqkms) {
        if (wqkms instanceof ConstantWQKms) {
            return;
        }
        if (this.master instanceof WINFOArtifact) {
            writer.addColumn(getColumnTitle((WINFOArtifact) this.master, wqkms));
        } else {
            writer.addColumn(wqkms.getName());
        }
    }

    @Override
    protected void writePDF(final OutputStream out) {
        log.debug("write PDF");

        final boolean isQ = isQ();
        final MetaAndTableJRDataSource source = new MetaAndTableJRDataSource();

        final boolean isFixationAnalysis = this.master instanceof AbstractFixBunduArtifact;
        final boolean doWaterlevelAtGaugeOutput = isQ && isFixationAnalysis;

        final String jasperFile = doWaterlevelAtGaugeOutput ? "/jasper/templates/fix_waterlevel.jrxml" : "/jasper/templates/waterlevel.jrxml";

        ((D4EArtifact) this.master).getData("calculation.mode");
        if ((this.master instanceof WINFOArtifact)) {
            addMetaData(source, "calc.surface.curve", isQ); // Wasserspiegellage
        } else if (this.master instanceof AbstractFixBunduArtifact) {
            addMetaData(source, ((AbstractFixBunduArtifact) this.master).getCalculationModeString(), isQ);
        }

        try {
            final List<String[]> sorted = getRows(); // Custom Result could be nice, too...
            for (final String[] list : sorted) {
                source.addData(list);
            }

            final JasperReporter reporter = new JasperReporter();

            final JasperDesigner d = reporter.addReport(jasperFile, source);
            d.removeColumn("delete"); // I don't want to mess with getRows(), so I prefer deleting the unwanted row directly in the report.

            reporter.exportPDF(this.out);
        }
        catch (final JRException je) {
            log.warn("Error generating PDF Report!", je);
        }

    }

    private List<String[]> getRows() {
        final List<String[]> list = new ArrayList<>();
        final WQ_MODE mode = RiverUtils.getWQMode((D4EArtifact) this.master);
        final boolean atGauge = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.WGAUGE;
        final boolean isQ = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.QFREE;

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

        for (final WQKms[] tmp : this.data) {
            for (final WQKms wqkms : tmp) {
                list.addAll(getRows2(wqkms, atGauge, isQ));
                final 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 (final WQKms wqkms : this.officalFixings) {
            list.addAll(getRows2(filterWQKms(wqkms, first, last), atGauge, isQ));
        }
        return list;
    }

    private List<String[]> getRows2(final WQKms wqkms, final boolean atGauge, final boolean isQ) {
        log.debug("WaterlevelExporter.addWKmsData"); // OLD CODE :-/

        final List<String[]> list = new ArrayList<>();
        // Skip constant data.
        if (wqkms instanceof ConstantWQKms) {
            return null;
        }

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

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

        final D4EArtifact flys = (D4EArtifact) this.master;
        final RangeAccess rangeAccess = new RangeAccess(flys);

        final List<Gauge> gauges = RiverUtils.getGauges(flys);

        Gauge gauge = rangeAccess.getRiver().determineRefGauge(rangeAccess.getKmRange(), rangeAccess.isRange());
        // REMARK gauge may be null when rangeAccess starts outside any gauge range
        if (gauge == null)
            gauge = rangeAccess.getRiver().determineRefGauge(wqkms.getKms(), rangeAccess.isRange());

        final String notinrange = msg(CSV_NOT_IN_GAUGE_RANGE, DEFAULT_CSV_NOT_IN_GAUGE_RANGE);
        final String gaugeName = gauge != null ? gauge.getName() : notinrange;

        final double a = gauge != null ? gauge.getRange().getA().doubleValue() : Double.NaN;
        final double b = gauge != null ? gauge.getRange().getB().doubleValue() : Double.NaN;

        final WaterlevelDescriptionBuilder wldb = new WaterlevelDescriptionBuilder(flys, this.context);

        String desc = wldb.getDesc(wqkms);// class getDesc(wqkms, isQ);

        Segment lastSegment = null;

        final List<Segment> segments = getSegments(flys);

        final boolean isFixRealize = isFixrealize(segments);

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

        Gauge lastGauge = null;

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

            final double station = result[2];
            final double q = result[1];
            final double w = result[0];

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

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

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

                gaugeN = found != null ? found.getName() : notinrange;
                lastGauge = found;

                wAtGauge = this.getWaterlevel(q, found); // THIS IS NEW (and makes common super method difficult)
            } else {
                // TODO issue1114: Take correct gauge
                gaugeN = station >= a && station <= b ? gaugeName : notinrange;
                wAtGauge = "";
            }

            if (atGauge) {
                list.add(new String[] { kmf.format(station), wf.format(w), wAtGauge, qf.format(RiverUtils.roundQ(q)), desc,
                        RiverUtils.getLocationDescription(flys, station), gaugeN });
            } else {
                list.add(new String[] { kmf.format(station), wf.format(w), wAtGauge, qf.format(RiverUtils.roundQ(q)), desc,
                        RiverUtils.getLocationDescription(flys, station), gaugeN });
            }
        }

        return list;
    }

    protected final void addMetaData(final MetaAndTableJRDataSource source, final String calculation, final boolean isQ) {
        final D4EArtifact flys = (D4EArtifact) this.master;
        final String user = CalculationUtils.findArtifactUser(this.context, flys);
        final RangeAccess ra = new RangeAccess(flys);
        final RiverInfo ri = new RiverInfo(ra.getRiver());

        final DefaultCalculationResults results = new DefaultCalculationResults(msg(calculation), user, ri, ra.getRange());
        final ExportContextPDF contextPdf = new ExportContextPDF(this.context, results);
        contextPdf.addJRMetaDataDefaults(source);
        contextPdf.addJRMetaDataForModules(source);

        /* column headings */
        contextPdf.addJRMetadata(source, "station_header", GeneralResultType.station);
        contextPdf.addJRMetadata(source, "fix_w", msg(CSV_W_HEADER, DEFAULT_CSV_W_HEADER, new Object[] { ri.getWstUnit() }));
        contextPdf.addJRMetadata(source, "w_at_gauge_header", msg("fix.export.csv.w_at_gauge"));
        contextPdf.addJRMetadata(source, "fix_q", msg(CSV_Q_HEADER));

        contextPdf.addJRMetadata(source, "waterlevel_name_header", msg("common.export.csv.header.mainvalue_label"));
        contextPdf.addJRMetadata(source, "location_header", msg("common.export.csv.header.location"));

        // FIXME: use WaterlevelDescriptionBuilder instead and also remove all this duplicate code.
        final String waterlevelOrBezeichnung = (isQ ? msg(CSV_Q_DESC_HEADER, DEFAULT_CSV_Q_DESC_HEADER) : msg(CSV_W_DESC_HEADER, DEFAULT_CSV_W_DESC_HEADER));
        contextPdf.addJRMetadata(source, "w_at_gauge_header_2", waterlevelOrBezeichnung);

        // msg("export.waterlevel.csv.header.w.desc")); (old. delete)

        contextPdf.addJRMetadata(source, "location_header", msg("common.export.csv.header.location"));
        contextPdf.addJRMetadata(source, "gauge_header", msg("common.export.csv.header.gauge"));

    }

    private WKmsJRDataSource createJRData() {
        final WKmsJRDataSource source = new WKmsJRDataSource();

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

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

        addMetaData(source);
        for (final WQKms[] tmp : this.data) {
            for (final WQKms wqkms : tmp) {
                addWKmsData(wqkms, atGauge, isQ, source);
                final 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 (final WQKms wqkms : this.officalFixings) {
            addWKmsData(filterWQKms(wqkms, first, last), atGauge, isQ, source);
        }
        return source;
    }

    public void addMetaData(final WKmsJRDataSource source) {
        final CallMeta meta = this.context.getMeta();

        final D4EArtifact flys = (D4EArtifact) this.master;

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

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

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

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

        source.addMetaData("w_at_gauge_header", Resources.getMsg(meta, "fix.export.csv.w_at_gauge")); // dürfte kein Problem sein für Vorlagen, die kein
                                                                                                      // "w_at_gauge"
        // haben

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

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

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

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

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

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

        final D4EArtifact flys = (D4EArtifact) this.master;
        final RangeAccess rangeAccess = new RangeAccess(flys);

        final Gauge gauge = rangeAccess.getRiver().determineRefGauge(rangeAccess.getKmRange(), rangeAccess.isRange());

        final String gaugeName = gauge.getName();
        String desc = "";
        final String notinrange = msg(CSV_NOT_IN_GAUGE_RANGE, DEFAULT_CSV_NOT_IN_GAUGE_RANGE);

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

        desc = getDesc(wqkms, isQ);
        final 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 });
            }

        }

        final long stopTime = System.currentTimeMillis();

        if (log.isDebugEnabled()) {
            log.debug("Writing PDF data took " + (stopTime - startTime) / 1000f + " secs.");
        }
    }

    protected final String getWaterlevel(final double discharge, final Gauge gauge) {
        final NumberFormat formatter = Formatter.getIntegerFormatter(this.context);
        final Double waterlevel = this.getWforGaugeAndQ(gauge, discharge);
        if (waterlevel != null)
            return formatter.format(waterlevel);
        return "";
    }

    private Double getWforGaugeAndQ(final Gauge gauge, final double q) {

        final String key = gauge != null ? gauge.getName() + String.valueOf(q) : null;
        if (!this.gaugeQ_W_Map.containsKey(key) && key != null) {

            // (Pos 19.1 b)
            final DischargeTable dt = gauge.fetchRecentDischargeTable();
            final double[][] table = DischargeTables.loadDischargeTableValues(dt);

            final double[] qs = DischargeTables.getWsForQ(table, q);

            if (qs != null && qs.length > 0) {
                this.gaugeQ_W_Map.put(key, qs[0]);
            }
        }
        return this.gaugeQ_W_Map.get(key);
    }
}

http://dive4elements.wald.intevation.org