view artifacts/src/main/java/org/dive4elements/river/artifacts/model/WstValueTable.java @ 9286:bcbae86ce7b3

Improved flood duration curve calculation as specified by BfG
author mschaefer
date Mon, 23 Jul 2018 19:20:06 +0200
parents 5e38e2924c07
children 853f2dafc16e
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.artifacts.model;

import static org.dive4elements.river.backend.utils.EpsilonComparator.CMP;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.commons.math.ArgumentOutsideDomainException;
import org.apache.commons.math.analysis.interpolation.SplineInterpolator;
import org.apache.commons.math.analysis.polynomials.PolynomialSplineFunction;
import org.apache.commons.math.exception.MathIllegalArgumentException;
import org.apache.log4j.Logger;
import org.dive4elements.river.artifacts.math.Function;
import org.dive4elements.river.artifacts.math.Linear;
import org.dive4elements.river.utils.DoubleUtil;

import gnu.trove.TDoubleArrayList;

/**
 * W, Q and km data from database 'wsts' spiced with interpolation algorithms.
 */
public class WstValueTable
implements   Serializable
{
    private static Logger log = Logger.getLogger(WstValueTable.class);

    public static final int DEFAULT_Q_STEPS = 500;

    public static final int RELATE_WS_SAMPLES = 200;

    public static final double W_EPSILON = 0.000001;

    /**
     * A Column in the table, typically representing one measurement session.
     */
    public static final class Column
    implements                Serializable
    {
        protected String name;

        protected QRangeTree qRangeTree;

        public Column() {
        }

        public Column(final String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }

        public void setName(final String name) {
            this.name = name;
        }

        public QRangeTree getQRangeTree() {
            return this.qRangeTree;
        }

        public void setQRangeTree(final QRangeTree qRangeTree) {
            this.qRangeTree = qRangeTree;
        }
    } // class Column

    /**
     * A (weighted) position used for interpolation.
     */
    public static final class QPosition {

        protected int    index;
        protected double weight;

        public QPosition() {
        }

        public QPosition(final int index, final double weight) {
            this.index  = index;
            this.weight = weight;
        }

        public QPosition set(final int index, final double weight) {
            this.index  = index;
            this.weight = weight;
            return this;
        }

    } // class Position

    public static final class SplineFunction {

        public PolynomialSplineFunction spline;
        public double []                splineQs;
        public double []                splineWs;

        public SplineFunction(
                final PolynomialSplineFunction spline,
                final double []                splineQs,
                final double []                splineWs
                ) {
            this.spline   = spline;
            this.splineQs = splineQs;
            this.splineWs = splineWs;
        }

        public double [][] sample(
                final int         numSamples,
                final double      km,
                final Calculation errors
                ) {
            final double minQ = getQMin();
            final double maxQ = getQMax();

            final double [] outWs = new double[numSamples];
            final double [] outQs = new double[numSamples];

            Arrays.fill(outWs, Double.NaN);
            Arrays.fill(outQs, Double.NaN);

            final double stepWidth = (maxQ - minQ)/numSamples;

            try {
                double q = minQ;
                for (int i = 0; i < outWs.length; ++i, q += stepWidth) {
                    outWs[i] = this.spline.value(outQs[i] = q);
                }
            }
            catch (final ArgumentOutsideDomainException aode) {
                if (errors != null) {
                    errors.addProblem(km, "spline.interpolation.failed");
                }
                log.error("spline interpolation failed.", aode);
            }

            return new double [][] { outWs, outQs };
        }

        public double getQMin() {
            return Math.min(this.splineQs[0], this.splineQs[this.splineQs.length-1]);
        }

        public double getQMax() {
            return Math.max(this.splineQs[0], this.splineQs[this.splineQs.length-1]);
        }

        /** Constructs a continues index between the columns to Qs. */
        public PolynomialSplineFunction createIndexQRelation() {

            final double [] indices = new double[this.splineQs.length];
            for (int i = 0; i < indices.length; ++i) {
                indices[i] = i;
            }

            try {
                final SplineInterpolator interpolator = new SplineInterpolator();
                return interpolator.interpolate(indices, this.splineQs);
            }
            catch (final MathIllegalArgumentException miae) {
                // Ignore me!
            }
            return null;
        }
    } // class SplineFunction

    /**
     * A row, typically a position where measurements were taken.
     */
    public static final class Row
    implements                Serializable, Comparable<Row>
    {
        double    km;
        double [] ws;

        public Row() {
        }

        public Row(final double km) {
            this.km = km;
        }

        public Row(final double km, final double [] ws) {
            this(km);
            this.ws = ws;
        }

        /**
         * Sort Qs and Ws for this Row over Q.
         */
        private double[][] getSortedWQ(
                final WstValueTable table,
                final Calculation   errors
                ) {
            final int W = this.ws.length;

            if (W < 1) {
                if (errors != null) {
                    errors.addProblem(this.km, "no.ws.found");
                }
                return new double[][] {{Double.NaN}, {Double.NaN}};
            }

            final double [] sortedWs = this.ws;
            final double [] sortedQs = new double[W];

            for (int i=0; i < W; ++i) {
                final double q = table.getQIndex(i, this.km);
                if (Double.isNaN(q) && errors != null) {
                    errors.addProblem(
                            this.km, "no.q.found.in.column", i+1);
                }
                sortedQs[i] = q;
            }

            DoubleUtil.sortByFirst(sortedQs, sortedWs);

            return new double[][] { sortedWs, sortedQs };
        }

        /**
         * Return an array of Qs and Ws for the given km between
         * this Row and another, sorted over Q.
         */
        private double[][] getSortedWQ(
                final Row           other,
                final double        km,
                final WstValueTable table,
                final Calculation   errors
                ) {
            final int W = Math.min(this.ws.length, other.ws.length);

            if (W < 1) {
                if (errors != null) {
                    errors.addProblem("no.ws.found");
                }
                return new double[][] {{Double.NaN}, {Double.NaN}};
            }

            final double factor = Linear.factor(km, this.km, other.km);

            final double [] sortedQs = new double[W];
            final double [] sortedWs = new double[W];

            for (int i = 0; i < W; ++i) {
                final double wws = Linear.weight(factor, this.ws[i], other.ws[i]);
                final double wqs = table.getQIndex(i, km);

                if (Double.isNaN(wws) || Double.isNaN(wqs)) {
                    if (errors != null) {
                        errors.addProblem(km, "cannot.find.w.or.q");
                    }
                }

                sortedWs[i] = wws;
                sortedQs[i] = wqs;
            }

            DoubleUtil.sortByFirst(sortedQs, sortedWs);

            return new double[][] { sortedWs, sortedQs };
        }

        /**
         * Find Qs matching w in an array of Qs and Ws sorted over Q.
         */
        private double[] findQsForW(final double w, final double[][] sortedWQ) {
            final int W = sortedWQ[0].length;

            final double[] sortedQs = sortedWQ[1];
            final double[] sortedWs = sortedWQ[0];

            final TDoubleArrayList qs = new TDoubleArrayList();

            if (W > 0 && Math.abs(sortedWs[0]-w) < W_EPSILON) {
                final double q = sortedQs[0];
                if (!Double.isNaN(q)) {
                    qs.add(q);
                }
            }

            for (int i = 1; i < W; ++i) {
                final double w2 = sortedWs[i];
                if (Double.isNaN(w2)) {
                    continue;
                }
                if (Math.abs(w2-w) < W_EPSILON) {
                    final double q = sortedQs[i];
                    if (!Double.isNaN(q)) {
                        qs.add(q);
                    }
                    continue;
                }
                final double w1 = sortedWs[i-1];
                if (Double.isNaN(w1)) {
                    continue;
                }

                if (w < Math.min(w1, w2) || w > Math.max(w1, w2)) {
                    continue;
                }

                final double q1 = sortedQs[i-1];
                final double q2 = sortedQs[i];
                if (Double.isNaN(q1) || Double.isNaN(q2)) {
                    continue;
                }

                final double q = Linear.linear(w, w1, w2, q1, q2);
                qs.add(q);
            }

            return qs.toNativeArray();
        }

        /**
         * Compare according to place of measurement (km).
         */
        @Override
        public int compareTo(final Row other) {
            return CMP.compare(this.km, other.km);
        }

        /**
         * Interpolate Ws, given Qs and a km.
         *
         * @param iqs Given ("input") Qs.
         * @param ows Resulting ("output") Ws.
         * @param table Table of which to use data for interpolation.
         */
        public void interpolateW(
                final Row           other,
                final double        km,
                final double []     iqs,
                final double []     ows,
                final WstValueTable table,
                final Calculation   errors
                ) {
            final double kmWeight = Linear.factor(km, this.km, other.km);

            final QPosition qPosition = new QPosition();

            for (int i = 0; i < iqs.length; ++i) {
                if (table.getQPosition(km, iqs[i], qPosition) != null) {
                    final double wt =       getW(qPosition);
                    final double wo = other.getW(qPosition);
                    if (Double.isNaN(wt) || Double.isNaN(wo)) {
                        if (errors != null) {
                            errors.addProblem(
                                    km, "cannot.find.w.for.q", iqs[i]);
                        }
                        ows[i] = Double.NaN;
                    }
                    else {
                        ows[i] = Linear.weight(kmWeight, wt, wo);
                    }
                }
                else {
                    if (errors != null) {
                        errors.addProblem(km, "cannot.find.q", iqs[i]);
                    }
                    ows[i] = Double.NaN;
                }
            }
        }


        public SplineFunction createSpline(
                final WstValueTable table,
                final Calculation   errors
                ) {
            final double[][] sortedWQ = getSortedWQ(table, errors);

            try {
                final SplineInterpolator interpolator = new SplineInterpolator();
                final PolynomialSplineFunction spline =
                        interpolator.interpolate(sortedWQ[1], sortedWQ[0]);

                return new SplineFunction(spline, sortedWQ[1], this.ws);
            }
            catch (final MathIllegalArgumentException miae) {
                if (errors != null) {
                    errors.addProblem(this.km, "spline.creation.failed");
                }
                log.error("spline creation failed", miae);
            }
            return null;
        }

        public SplineFunction createSpline(
                final Row           other,
                final double        km,
                final WstValueTable table,
                final Calculation   errors
                ) {
            final double[][] sortedWQ = getSortedWQ(other, km, table, errors);

            final SplineInterpolator interpolator = new SplineInterpolator();

            try {
                final PolynomialSplineFunction spline =
                        interpolator.interpolate(sortedWQ[1], sortedWQ[0]);

                return new SplineFunction(spline, sortedWQ[1], sortedWQ[0]);
            }
            catch (final MathIllegalArgumentException miae) {
                if (errors != null) {
                    errors.addProblem(km, "spline.creation.failed");
                }
                log.error("spline creation failed", miae);
            }

            return null;
        }

        public double [][] interpolateWQ(
                final Row           other,
                final double        km,
                final int           steps,
                final WstValueTable table,
                final Calculation   errors
                ) {
            final SplineFunction sf = createSpline(other, km, table, errors);

            return sf != null
                    ? sf.sample(steps, km, errors)
                            : new double[2][0];
        }


        public double [][] interpolateWQ(
                final int           steps,
                final WstValueTable table,
                final Calculation   errors
                ) {
            final SplineFunction sf = createSpline(table, errors);

            return sf != null
                    ? sf.sample(steps, this.km, errors)
                            : new double[2][0];
        }


        public double getW(final QPosition qPosition) {
            final int    index  = qPosition.index;
            final double weight = qPosition.weight;

            return weight == 1.0
                    ? this.ws[index]
                            : Linear.weight(weight, this.ws[index-1], this.ws[index]);
        }

        public double getW(
                final Row       other,
                final double    km,
                final QPosition qPosition
                ) {
            final double kmWeight = Linear.factor(km, this.km, other.km);

            final int    index  = qPosition.index;
            final double weight = qPosition.weight;

            double tw, ow;

            if (weight == 1.0) {
                tw = this.ws[index];
                ow = other.ws[index];
            }
            else {
                tw = Linear.weight(weight, this.ws[index-1], this.ws[index]);
                ow = Linear.weight(weight, other.ws[index-1], other.ws[index]);
            }

            return Linear.weight(kmWeight, tw, ow);
        }

        public double [] findQsForW(
                final double        w,
                final WstValueTable table,
                final Calculation   errors
                ) {
            log.debug("Find Qs for given W at tabulated km " + this.km);
            return findQsForW(w, getSortedWQ(table, errors));
        }

        public double [] findQsForW(
                final Row           other,
                final double        w,
                final double        km,
                final WstValueTable table,
                final Calculation   errors
                ) {
            log.debug("Find Qs for given W at non-tabulated km " + km);
            return findQsForW(w, getSortedWQ(other, km, table, errors));
        }

        public double [] getMinMaxW(final double [] result) {
            double minW =  Double.MAX_VALUE;
            double maxW = -Double.MAX_VALUE;
            for (int i = 0; i < this.ws.length; ++i) {
                final double w = this.ws[i];
                if (w < minW) minW = w;
                if (w > maxW) maxW = w;
            }
            result[0] = minW;
            result[1] = maxW;
            return result;
        }

        public double [] getMinMaxW(final Row other, final double km, final double [] result) {
            final double [] m1 = this .getMinMaxW(new double [2]);
            final double [] m2 = other.getMinMaxW(new double [2]);
            final double factor = Linear.factor(km, this.km, other.km);
            result[0] = Linear.weight(factor, m1[0], m2[0]);
            result[1] = Linear.weight(factor, m1[1], m2[1]);
            return result;
        }
    } // class Row

    /** Rows in table. */
    protected List<Row> rows;

    /** Columns in table. */
    protected Column [] columns;

    public WstValueTable() {
        this.rows = new ArrayList<>();
    }

    public WstValueTable(final Column [] columns) {
        this();
        this.columns = columns;
    }

    /**
     * @param columns The WST-columns.
     * @param rows A list of Rows that must be sorted by km.
     */
    public WstValueTable(final Column [] columns, final List<Row> rows) {
        this.columns = columns;
        this.rows    = rows;
    }

    public Column [] getColumns() {
        return this.columns;
    }

    /**
     * @param km Given kilometer.
     * @param qs Given Q values.
     * @param ws output parameter.
     */
    public double [] interpolateW(final double km, final double [] qs, final double [] ws) {
        return interpolateW(km, qs, ws, null);
    }


    /**
     * @param ws (output parameter), gets returned.
     * @return output parameter ws.
     */
    public double [] interpolateW(
            final double      km,
            final double []   qs,
            final double []   ws,
            final Calculation errors
            ) {
        int rowIndex = Collections.binarySearch(this.rows, new Row(km));

        final QPosition qPosition = new QPosition();

        if (rowIndex >= 0) { // direct row match
            final Row row = this.rows.get(rowIndex);
            for (int i = 0; i < qs.length; ++i) {
                if (getQPosition(km, qs[i], qPosition) == null) {
                    if (errors != null) {
                        errors.addProblem(km, "cannot.find.q", qs[i]);
                    }
                    ws[i] = Double.NaN;
                }
                else {
                    if (Double.isNaN(ws[i] = row.getW(qPosition))
                            && errors != null) {
                        errors.addProblem(
                                km, "cannot.find.w.for.q", qs[i]);
                    }
                }
            }
        }
        else { // needs bilinear interpolation
            rowIndex = -rowIndex -1;

            if (rowIndex < 1 || rowIndex >= this.rows.size()) {
                // do not extrapolate
                Arrays.fill(ws, Double.NaN);
                if (errors != null) {
                    errors.addProblem(km, "km.not.found");
                }
            }
            else {
                final Row r1 = this.rows.get(rowIndex-1);
                final Row r2 = this.rows.get(rowIndex);
                r1.interpolateW(r2, km, qs, ws, this, errors);
            }
        }

        return ws;
    }

    /**
     * Interpolates a W for a km using a Q column position
     */
    public double interpolateW(final double km, final QPosition qPosition, final Calculation errors) {

        int rowIndex = Collections.binarySearch(this.rows, new Row(km));

        if (rowIndex >= 0) { // direct row match
            final Row row = this.rows.get(rowIndex);
            return row.getW(qPosition);
        }
        else { // needs bilinear interpolation
            rowIndex = -rowIndex - 1;
            if ((rowIndex <= 0) || (rowIndex >= this.rows.size()))
                return Double.NaN;
            else {
                final Row r1 = this.rows.get(rowIndex - 1);
                final Row r2 = this.rows.get(rowIndex);
                final double w1 = r1.getW(qPosition);
                final double w2 = r2.getW(qPosition);
                if (Double.isNaN(w1) || Double.isNaN(w2))
                    return Double.NaN;
                else {
                    final double kmWeight = Linear.factor(km, r1.km, r2.km);
                    return Linear.weight(kmWeight, w1, w2);
                }
            }
        }
    }

    public double [] getMinMaxQ(final double km) {
        return getMinMaxQ(km, new double [2]);
    }

    public double [] getMinMaxQ(final double km, final double [] result) {
        double minQ =  Double.MAX_VALUE;
        double maxQ = -Double.MAX_VALUE;

        for (int i = 0; i < this.columns.length; ++i) {
            final double q = this.columns[i].getQRangeTree().findQ(km);
            if (!Double.isNaN(q)) {
                if (q < minQ) minQ = q;
                if (q > maxQ) maxQ = q;
            }
        }

        if (minQ < Double.MAX_VALUE) {
            result[0] = minQ;
            result[1] = maxQ;
            return result;
        }

        return null;
    }

    public double [] getMinMaxQ(double from, double to, double step) {
        final double [] result = new double[2];

        double minQ =  Double.MAX_VALUE;
        double maxQ = -Double.MAX_VALUE;

        if (from > to) {
            final double tmp = from;
            from = to;
            to = tmp;
        }

        step = Math.max(Math.abs(step), 0.0001);

        double d = from;
        for (; d <= to; d += step) {
            if (getMinMaxQ(d, result) != null) {
                if (result[0] < minQ) minQ = result[0];
                if (result[1] > maxQ) maxQ = result[1];
            }
        }

        if (d != to) {
            if (getMinMaxQ(to, result) != null) {
                if (result[0] < minQ) minQ = result[0];
                if (result[1] > maxQ) maxQ = result[1];
            }
        }

        return minQ < Double.MAX_VALUE
                ? new double [] { minQ, maxQ }
        : null;
    }

    public double [] getMinMaxW(final double km) {
        return getMinMaxW(km, new double [2]);

    }
    public double [] getMinMaxW(final double km, final double [] result) {
        int rowIndex = Collections.binarySearch(this.rows, new Row(km));

        if (rowIndex >= 0) {
            return this.rows.get(rowIndex).getMinMaxW(result);
        }

        rowIndex = -rowIndex -1;

        if (rowIndex < 1 || rowIndex >= this.rows.size()) {
            // do not extrapolate
            return null;
        }

        final Row r1 = this.rows.get(rowIndex-1);
        final Row r2 = this.rows.get(rowIndex);

        return r1.getMinMaxW(r2, km, result);
    }

    public double [] getMinMaxW(double from, double to, double step) {
        final double [] result = new double[2];

        double minW =  Double.MAX_VALUE;
        double maxW = -Double.MAX_VALUE;

        if (from > to) {
            final double tmp = from;
            from = to;
            to = tmp;
        }

        step = Math.max(Math.abs(step), 0.0001);

        double d = from;
        for (; d <= to; d += step) {
            if (getMinMaxW(d, result) != null) {
                if (result[0] < minW) minW = result[0];
                if (result[1] > maxW) maxW = result[1];
            }
        }

        if (d != to) {
            if (getMinMaxW(to, result) != null) {
                if (result[0] < minW) minW = result[0];
                if (result[1] > maxW) maxW = result[1];
            }
        }

        return minW < Double.MAX_VALUE
                ? new double [] { minW, maxW }
        : null;
    }

    /**
     * Interpolate W and Q values at a given km.
     */
    public double [][] interpolateWQ(final double km) {
        return interpolateWQ(km, null);
    }

    /**
     * Interpolate W and Q values at a given km.
     *
     * @param errors where to store errors.
     *
     * @return double double array, first index Ws, second Qs.
     */
    public double [][] interpolateWQ(final double km, final Calculation errors) {
        return interpolateWQ(km, DEFAULT_Q_STEPS, errors);
    }


    /**
     * Interpolate W and Q values at a given km.
     */
    public double [][] interpolateWQ(
            final double km,
            final int steps,
            final Calculation errors
            ) {
        int rowIndex = Collections.binarySearch(this.rows, new Row(km));

        if (rowIndex >= 0) { // direct row match
            final Row row = this.rows.get(rowIndex);
            return row.interpolateWQ(steps, this, errors);
        }

        rowIndex = -rowIndex -1;

        if (rowIndex < 1 || rowIndex >= this.rows.size()) {
            // do not extrapolate
            if (errors != null) {
                errors.addProblem(km, "km.not.found");
            }
            return new double[2][0];
        }

        final Row r1 = this.rows.get(rowIndex-1);
        final Row r2 = this.rows.get(rowIndex);

        return r1.interpolateWQ(r2, km, steps, this, errors);
    }

    public boolean interpolate(
            final double    km,
            final double [] out,
            final QPosition qPosition,
            final Function  qFunction
            ) {
        final int R1 = this.rows.size()-1;

        out[1] = qFunction.value(getQ(qPosition, km));

        if (Double.isNaN(out[1])) {
            return false;
        }

        final QPosition nPosition = new QPosition();
        if (getQPosition(km, out[1], nPosition) == null) {
            return false;
        }

        int rowIndex = Collections.binarySearch(this.rows, new Row(km));

        if (rowIndex >= 0) {
            // direct row match
            out[0] = this.rows.get(rowIndex).getW(nPosition);
            return !Double.isNaN(out[0]);
        }

        rowIndex = -rowIndex -1;

        if (rowIndex < 1 || rowIndex > R1) {
            // do not extrapolate
            return false;
        }

        final Row r1 = this.rows.get(rowIndex-1);
        final Row r2 = this.rows.get(rowIndex);
        out[0] = r1.getW(r2, km, nPosition);

        return !Double.isNaN(out[0]);
    }


    /**
     * Look up interpolation of a Q at given positions.
     *
     * @param q           the non-interpolated Q value.
     * @param referenceKm the reference km (e.g. gauge position).
     * @param kms         positions for which to interpolate.
     * @param ws          (output) resulting interpolated ws.
     * @param qs          (output) resulting interpolated qs.
     * @param errors      calculation object to store errors.
     */
    public QPosition interpolate(
            final double      q,
            final double      referenceKm,
            final double []   kms,
            final double []   ws,
            final double []   qs,
            final Calculation errors
            ) {
        return interpolate(
                q, referenceKm, kms, ws, qs, 0, kms.length, errors);
    }

    /**
     * Interpolate Q at given positions.
     * @param kms positions for which to calculate qs and ws
     * @param ws [out] calculated ws for kms
     * @param qs [out] looked up qs for kms.
     */
    public QPosition interpolate(
            final double      q,
            final double      referenceKm,
            final double []   kms,
            final double []   ws,
            final double []   qs,
            final int         startIndex,
            final int         length,
            final Calculation errors
            ) {
        final QPosition qPosition = getQPosition(referenceKm, q);

        if (qPosition == null) {
            // we cannot locate q at km
            Arrays.fill(ws, Double.NaN);
            Arrays.fill(qs, Double.NaN);
            if (errors != null) {
                errors.addProblem(referenceKm, "cannot.find.q", q);
            }
            return null;
        }

        final Row kmKey = new Row();

        final int R1 = this.rows.size()-1;

        for (int i = startIndex, end = startIndex+length; i < end; ++i) {

            if (Double.isNaN(qs[i] = getQ(qPosition, kms[i]))) {
                if (errors != null) {
                    errors.addProblem(kms[i], "cannot.find.q", q);
                }
                ws[i] = Double.NaN;
                continue;
            }

            kmKey.km = kms[i];
            int rowIndex = Collections.binarySearch(this.rows, kmKey);

            if (rowIndex >= 0) {
                // direct row match
                if (Double.isNaN(ws[i] = this.rows.get(rowIndex).getW(qPosition))
                        && errors != null) {
                    errors.addProblem(kms[i], "cannot.find.w.for.q", q);
                }
                continue;
            }

            rowIndex = -rowIndex -1;

            if (rowIndex < 1 || rowIndex > R1) {
                // do not extrapolate
                if (errors != null) {
                    errors.addProblem(kms[i], "km.not.found");
                }
                ws[i] = Double.NaN;
                continue;
            }
            final Row r1 = this.rows.get(rowIndex-1);
            final Row r2 = this.rows.get(rowIndex);

            if (Double.isNaN(ws[i] = r1.getW(r2, kms[i], qPosition))
                    && errors != null) {
                errors.addProblem(kms[i], "cannot.find.w.for.q", q);
            }
        }

        return qPosition;
    }

    /**
     * Linearly interpolate w at a km at a column of two rows.
     *
     * @param km   position for which to interpolate.
     * @param row1 first row.
     * @param row2 second row.
     * @param col  column-index at which to look.
     *
     * @return Linearly interpolated w, NaN if one of the given rows was null.
     */
    public static double linearW(final double km, final Row row1, final Row row2, final int col) {
        if (row1 == null || row2 == null) {
            return Double.NaN;
        }

        return Linear.linear(km,
                row1.km, row2.km,
                row1.ws[col], row2.ws[col]);
    }

    /**
     * Do interpolation/lookup of W and Q within columns (i.e. ignoring values
     * of other columns).
     * @param km position (km) at which to interpolate/lookup.
     * @return [[q0, q1, .. qx] , [w0, w1, .. wx]] (can contain NaNs)
     */
    public double [][] interpolateWQColumnwise(final double km) {
        log.debug("WstValueTable.interpolateWQColumnwise");
        final double [] qs = new double[this.columns.length];
        final double [] ws = new double[this.columns.length];

        // Find out row from where we will start searching.
        int rowIndex = Collections.binarySearch(this.rows, new Row(km));

        if (rowIndex < 0) {
            rowIndex = -rowIndex -1;
        }

        // TODO Beyond definition, we could stop more clever.
        if (rowIndex >= this.rows.size()) {
            rowIndex = this.rows.size() -1;
        }

        final Row startRow = this.rows.get(rowIndex);

        for (int col = 0; col < this.columns.length; col++) {
            qs[col] = this.columns[col].getQRangeTree().findQ(km);
            if (startRow.km == km && !Double.isNaN(startRow.ws[col])) {
                // Great. W is defined at km.
                ws[col] = startRow.ws[col];
                continue;
            }

            // Search neighbouring rows that define w at this col.
            Row rowBefore = null;
            Row rowAfter  = null;
            for (int before = rowIndex -1; before >= 0; before--) {
                if (!Double.isNaN(this.rows.get(before).ws[col])) {
                    rowBefore = this.rows.get(before);
                    break;
                }
            }
            if (rowBefore != null) {
                for (int after = rowIndex, R = this.rows.size();
                        after < R;
                        after++
                        ) {
                    if (!Double.isNaN(this.rows.get(after).ws[col])) {
                        rowAfter = this.rows.get(after);
                        break;
                    }
                }
            }

            ws[col] = linearW(km, rowBefore, rowAfter, col);
        }

        return new double [][] {qs, ws};
    }

    public double [] findQsForW(final double km, final double w, final Calculation errors) {

        int rowIndex = Collections.binarySearch(this.rows, new Row(km));

        if (rowIndex >= 0) {
            return this.rows.get(rowIndex).findQsForW(w, this, errors);
        }

        rowIndex = -rowIndex - 1;

        if (rowIndex < 1 || rowIndex >= this.rows.size()) {
            // Do not extrapolate.
            return new double[0];
        }

        // Needs bilinear interpolation.
        final Row r1 = this.rows.get(rowIndex-1);
        final Row r2 = this.rows.get(rowIndex);

        return r1.findQsForW(r2, w, km, this, errors);
    }

    protected SplineFunction createSpline(final double km, final Calculation errors) {

        int rowIndex = Collections.binarySearch(this.rows, new Row(km));

        if (rowIndex >= 0) {
            final SplineFunction sf = this.rows.get(rowIndex).createSpline(this, errors);
            if (sf == null && errors != null) {
                errors.addProblem(km, "cannot.create.wq.relation");
            }
            return sf;
        }

        rowIndex = -rowIndex - 1;

        if (rowIndex < 1 || rowIndex >= this.rows.size()) {
            // Do not extrapolate.
            if (errors != null) {
                errors.addProblem(km, "km.not.found");
            }
            return null;
        }

        // Needs bilinear interpolation.
        final Row r1 = this.rows.get(rowIndex-1);
        final Row r2 = this.rows.get(rowIndex);

        final SplineFunction sf = r1.createSpline(r2, km, this, errors);
        if (sf == null && errors != null) {
            errors.addProblem(km, "cannot.create.wq.relation");
        }

        return sf;
    }

    /** 'Bezugslinienverfahren' */
    public double [][] relateWs(
            final double      km1,
            final double      km2,
            final Calculation errors
            ) {
        return relateWs(km1, km2, RELATE_WS_SAMPLES, errors);
    }

    private static class ErrorHandler {

        boolean     hasErrors;
        Calculation errors;

        ErrorHandler(final Calculation errors) {
            this.errors = errors;
        }

        void error(final double km, final String key, final Object ... args) {
            if (this.errors != null && !this.hasErrors) {
                this.hasErrors = true;
                this.errors.addProblem(km, key, args);
            }
        }
    } // class ErrorHandler


    /* TODO: Add optimized methods of relateWs to relate one
     *       start km to many end kms. The index generation/spline stuff for
     *       the start km is always the same.
     */
    public double [][] relateWs(
            final double      km1,
            final double      km2,
            final int         numSamples,
            final Calculation errors
            ) {
        final SplineFunction sf1 = createSpline(km1, errors);
        if (sf1 == null) {
            return new double[2][0];
        }

        final SplineFunction sf2 = createSpline(km2, errors);
        if (sf2 == null) {
            return new double[2][0];
        }

        final PolynomialSplineFunction iQ1 = sf1.createIndexQRelation();
        if (iQ1 == null) {
            if (errors != null) {
                errors.addProblem(km1, "cannot.create.index.q.relation");
            }
            return new double[2][0];
        }

        final PolynomialSplineFunction iQ2 = sf2.createIndexQRelation();
        if (iQ2 == null) {
            if (errors != null) {
                errors.addProblem(km2, "cannot.create.index.q.relation");
            }
            return new double[2][0];
        }

        final int N = Math.min(sf1.splineQs.length, sf2.splineQs.length);
        final double stepWidth = N/(double)numSamples;

        final PolynomialSplineFunction qW1 = sf1.spline;
        final PolynomialSplineFunction qW2 = sf2.spline;

        final TDoubleArrayList ws1 = new TDoubleArrayList(numSamples);
        final TDoubleArrayList ws2 = new TDoubleArrayList(numSamples);
        final TDoubleArrayList qs1 = new TDoubleArrayList(numSamples);
        final TDoubleArrayList qs2 = new TDoubleArrayList(numSamples);

        final ErrorHandler err = new ErrorHandler(errors);

        int i = 0;
        for (double p = 0d; p <= N-1; p += stepWidth, ++i) {

            double q1;
            try {
                q1 = iQ1.value(p);
            }
            catch (final ArgumentOutsideDomainException aode) {
                err.error(km1, "w.w.qkm1.failed", p);
                continue;
            }

            double w1;
            try {
                w1 = qW1.value(q1);
            }
            catch (final ArgumentOutsideDomainException aode) {
                err.error(km1, "w.w.wkm1.failed", q1, p);
                continue;
            }

            double q2;
            try {
                q2 = iQ2.value(p);
            }
            catch (final ArgumentOutsideDomainException aode) {
                err.error(km2, "w.w.qkm2.failed", p);
                continue;
            }

            double w2;
            try {
                w2 = qW2.value(q2);
            }
            catch (final ArgumentOutsideDomainException aode) {
                err.error(km2, "w.w.wkm2.failed", q2, p);
                continue;
            }

            ws1.add(w1);
            ws2.add(w2);
            qs1.add(q1);
            qs2.add(q2);
        }

        return new double [][] {
            ws1.toNativeArray(),
            qs1.toNativeArray(),
            ws2.toNativeArray(),
            qs2.toNativeArray() };
    }

    public QPosition getQPosition(final double km, final double q) {
        return getQPosition(km, q, new QPosition());
    }

    public QPosition getQPosition(final double km, final double q, final QPosition qPosition) {

        if (this.columns.length == 0) {
            return null;
        }

        double qLast = this.columns[0].getQRangeTree().findQ(km);

        if (Math.abs(qLast - q) < 0.00001) {
            return qPosition.set(0, 1d);
        }

        for (int i = 1; i < this.columns.length; ++i) {
            final double qCurrent = this.columns[i].getQRangeTree().findQ(km);
            if (Math.abs(qCurrent - q) < 0.00001) {
                return qPosition.set(i, 1d);
            }

            double qMin, qMax;
            if (qLast < qCurrent) { qMin = qLast; qMax = qCurrent; }
            else                  { qMin = qCurrent; qMax = qLast; }

            if (q > qMin && q < qMax) {
                final double weight = Linear.factor(q, qLast, qCurrent);
                return qPosition.set(i, weight);
            }
            qLast = qCurrent;
        }

        return null;
    }

    public double getQIndex(final int index, final double km) {
        return this.columns[index].getQRangeTree().findQ(km);
    }

    public double getQ(final QPosition qPosition, final double km) {
        final int    index  = qPosition.index;
        final double weight = qPosition.weight;

        if (weight == 1d) {
            return this.columns[index].getQRangeTree().findQ(km);
        }
        final double q1 = this.columns[index-1].getQRangeTree().findQ(km);
        final double q2 = this.columns[index  ].getQRangeTree().findQ(km);
        return Linear.weight(weight, q1, q2);
    }

    public double [][] interpolateTabulated(final double km) {
        return interpolateTabulated(km, new double[2][this.columns.length]);
    }

    public double [][] interpolateTabulated(final double km, final double [][] result) {

        int rowIndex = Collections.binarySearch(this.rows, new Row(km));

        if (rowIndex >= 0) {
            // Direct hit -> copy ws.
            final Row row = this.rows.get(rowIndex);
            System.arraycopy(
                    row.ws, 0, result[0], 0,
                    Math.min(row.ws.length, result[0].length));
        }
        else {
            rowIndex = -rowIndex -1;
            if (rowIndex < 1 || rowIndex >= this.rows.size()) {
                // Out of bounds.
                return null;
            }
            // Interpolate ws.
            final Row r1 = this.rows.get(rowIndex-1);
            final Row r2 = this.rows.get(rowIndex);
            final double factor = Linear.factor(km, r1.km, r2.km);
            Linear.weight(factor, r1.ws, r2.ws, result[0]);
        }

        final double [] qs = result[1];
        for (int i = Math.min(qs.length, this.columns.length)-1; i >= 0; --i) {
            qs[i] = this.columns[i].getQRangeTree().findQ(km);
        }
        return result;
    }


    /** True if no QRange is given or Q equals zero. */
    public boolean hasEmptyQ() {
        for (final Column column: this.columns) {
            if (column.getQRangeTree() == null) {
                return true;
            }
            else {
                if (Math.abs(column.getQRangeTree().maxQ()) <= 0.01d) {
                    return true;
                }
            }
        }

        if (this.columns.length == 0) {
            log.warn("No columns in WstValueTable.");
        }

        return false;
    }


    /** Find ranges that are between km1 and km2 (inclusive?) */
    public List<Range> findSegments(final double km1, final double km2) {
        return this.columns.length != 0
                ? this.columns[this.columns.length-1].getQRangeTree().findSegments(km1, km2)
                        : Collections.<Range>emptyList();
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org