view artifacts/src/main/java/org/dive4elements/river/artifacts/services/DynamicMainValuesService.java @ 9396:6ebc9357550c

Fixed: bundu bzws start year plus one
author mschaefer
date Mon, 13 Aug 2018 17:15:05 +0200
parents 0255c51283a4
children bc9a45d2b1fa
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.services;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.dive4elements.artifacts.CallMeta;
import org.dive4elements.artifacts.GlobalContext;
import org.dive4elements.artifacts.common.ArtifactNamespaceContext;
import org.dive4elements.artifacts.common.utils.DateUtils;
import org.dive4elements.artifacts.common.utils.XMLUtils;
import org.dive4elements.river.backend.SessionHolder;
import org.dive4elements.river.model.Gauge;
import org.dive4elements.river.model.MainValue;
import org.dive4elements.river.model.MainValueType;
import org.dive4elements.river.model.MainValueType.MainValueTypeKey;
import org.dive4elements.river.model.NamedMainValue;
import org.dive4elements.river.model.OfficialLine;
import org.dive4elements.river.model.River;
import org.dive4elements.river.model.TimeInterval;
import org.dive4elements.river.model.sinfo.DailyDischargeValue;
import org.dive4elements.river.model.sinfo.DailyDischargeValue.OrderByField;
import org.dive4elements.river.utils.DoubleUtil;
import org.hibernate.Session;
import org.w3c.dom.Document;

import gnu.trove.TDoubleArrayList;

/**
 * This service returns the main values of a river's gauge based on the start
 * and end point of the river.
 *
 * @author <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
 */
public class DynamicMainValuesService extends AbstractMainValuesService {

    private static final long serialVersionUID = 1L;

    private static final String XPATH_START_YEAR = "/art:mainvalues/art:startYear/text()";

    private static final String XPATH_END_YEAR = "/art:mainvalues/art:endYear/text()";

    @Override
    public Document doProcess(final Document data, final GlobalContext context, final CallMeta callMeta) {
        try {

            final River river = getRequestedRiver(data);
            final Gauge gauge = getRequestedGauge(data, river);
            final Date startTime = getRequestedStartYear(data);
            final Date endTime = getRequestedEndYear(data);

            final List<MainValue> mainValues = getMainValues(river, gauge, startTime, endTime);

            return buildDocument(river, gauge, mainValues, context);
        }
        catch (final MainValuesServiceException e) {
            // e.printStackTrace();
            return error(e.getMessage());
        }
        catch (final Exception e) {
            e.printStackTrace();
            return error(e.getMessage());
        }
    }

    private Date getRequestedStartYear(final Document data) throws MainValuesServiceException {

        final String startStr = XMLUtils.xpathString(data, XPATH_START_YEAR, ArtifactNamespaceContext.INSTANCE);

        if (startStr == null)
            throw new MainValuesServiceException("no start year");

        try {
            final int year = Integer.parseInt(startStr);
            final Calendar cal = Calendar.getInstance();
            cal.clear();
            cal.set(year, 0, 1);
            return cal.getTime();
        }
        catch (final NumberFormatException e) {
            e.printStackTrace();
            throw new MainValuesServiceException("invalid start year");
        }
    }

    private Date getRequestedEndYear(final Document data) throws MainValuesServiceException {

        final String endStr = XMLUtils.xpathString(data, XPATH_END_YEAR, ArtifactNamespaceContext.INSTANCE);

        if (endStr == null)
            throw new MainValuesServiceException("no end year");

        try {
            final int year = Integer.parseInt(endStr);
            final Calendar cal = Calendar.getInstance();
            cal.clear();
            cal.set(year, 11, 31);
            return cal.getTime();
        }
        catch (final NumberFormatException e) {
            e.printStackTrace();
            throw new MainValuesServiceException("invalid end year");
        }
    }

    /**
     * Computes a gauge's main values for a period of time based on its daily discharges stored in the database
     */
    protected List<MainValue> getMainValues(final River river, final Gauge gauge, final Date startTime, final Date endTime) throws MainValuesServiceException {

        final List<MainValue> mainValues = new ArrayList<>();
        computeMainDischargeValues(gauge, startTime, endTime, mainValues);
        return mainValues;
    }

    /**
     * Computes mnq, mq, mhq, hq5 and q(d=0..364) and adds them to a MainValue list
     */
    private void computeMainDischargeValues(final Gauge gauge, final Date startTime, final Date endTime, final List<MainValue> mainValues)
            throws MainValuesServiceException {

        // Query the gauge's daily Q values
        final List<DailyDischargeValue> qdvs = DailyDischargeValue.getValues(gauge, startTime, endTime, OrderByField.DAY);
        if (qdvs.isEmpty())
            throw new MainValuesServiceException("no daily discharge values for gauge " + gauge.getName() + " in the requested time period");
        // return; // TODO Fehlermeldung

        // Build yearly aggregates
        final TDoubleArrayList mnqs = new TDoubleArrayList();
        final TDoubleArrayList mqs = new TDoubleArrayList();
        int mqcnt = 0;
        final TDoubleArrayList mhqs = new TDoubleArrayList();
        for (int i = 0, j = 0; i <= qdvs.size() - 1; i++) {
            final DailyDischargeValue qdv = qdvs.get(i);
            if ((i >= 1) && isSameQYear(qdv.getDay(), qdvs.get(i - 1).getDay())) {
                // Continue aggregating the values of the current year
                mnqs.set(j, Math.min(mnqs.get(j), qdv.getDischarge().doubleValue()));
                mhqs.set(j, Math.max(mhqs.get(j), qdv.getDischarge().doubleValue()));
                mqs.set(j, mqs.get(j) + qdv.getDischarge().doubleValue());
                mqcnt++;
                if (i == qdvs.size() - 1)
                    mqs.set(j, mqs.get(j) / mqcnt);
            }
            else {
                // Complete mq aggregation
                if (mqcnt >= 1) {
                    mqs.set(j, mqs.get(j) / mqcnt);
                    j++;
                }
                // Start next year
                mnqs.add(qdv.getDischarge().doubleValue());
                mhqs.add(qdv.getDischarge().doubleValue());
                mqs.add(qdv.getDischarge().doubleValue());
                mqcnt = 1;
            }
        }

        // Compute arithmetic means of the yearly values
        final Session session = SessionHolder.HOLDER.get();
        final TimeInterval timeperiod = new TimeInterval(startTime, endTime);
        final double mnq = DoubleUtil.sum(mnqs.toNativeArray()) / mnqs.size();
        mainValues.add(createMainValue(gauge, fetchNamedQMainValue("MNQ", session), mnq, timeperiod));
        final double mq = DoubleUtil.sum(mqs.toNativeArray()) / mqs.size();
        mainValues.add(createMainValue(gauge, fetchNamedQMainValue("MQ", session), mq, timeperiod));
        final double mhq = DoubleUtil.sum(mhqs.toNativeArray()) / mhqs.size();
        mainValues.add(createMainValue(gauge, fetchNamedQMainValue("MHQ", session), mhq, timeperiod));

        // Compute hq5 - obsolete
        // mhqs.sort();
        // final double hq5 = mhqs.get((int) Math.ceil(4 * mhqs.size() / 5));
        // mainValues.add(createMainValue(gauge, fetchNamedQMainValue("HQ5", session), hq5, timeperiod));

        // Add HSQ-II from the gauge's main values
        final MainValue hsq2 = fetchHsqII(gauge, session);
        if (hsq2 != null)
            mainValues.add(hsq2);

        // Query the gauge's daily Q values and build a list sorted by ascending Q
        final TDoubleArrayList qs = new TDoubleArrayList();
        for (final DailyDischargeValue qdv : qdvs)
            qs.add(qdv.getDischarge().doubleValue());
        qs.sort();

        // Step through the sorted Q list and get the duration discharges
        final int yearCnt = DateUtils.getYearFromDate(endTime) - DateUtils.getYearFromDate(startTime) + 1;
        double glq20 = Double.NaN;
        for (int i = 0, k = 0; (i <= 364) && (k <= qs.size() - 1); i++, k += yearCnt) {
            final NamedMainValue nmv = fetchNamedQMainValue(i, session, mainValues.get(0).getMainValue().getType());
            if (nmv != null) {
                final double q = getDurationQ(qs, k);
                mainValues.add(createMainValue(gauge, nmv, q, timeperiod));
                if (i == 20)
                    glq20 = q;
            }
        }
        mainValues.add(createMainValue(gauge, fetchNamedQMainValue("GlQ", session), glq20, timeperiod));
    }

    /**
     * Checks year equality of two dates (calendar year)
     */
    private boolean isSameQYear(final Date a, final Date b) {
        final Calendar ca = Calendar.getInstance();
        ca.setTime(a);
        final Calendar cb = Calendar.getInstance();
        cb.setTime(b);
        return (ca.get(Calendar.YEAR) == cb.get(Calendar.YEAR));
    }

    /**
     * Fetches the gauge's HSQ-II from the database, or returns null
     */
    private MainValue fetchHsqII(final Gauge gauge, final Session session) {
        final NamedMainValue nmv = NamedMainValue.fetchByNameAndType("HSQ-II", MainValueTypeKey.UNKNOWN.getName(), session);
        if (nmv == null)
            return null;
        final List<MainValue> mvs = gauge.getMainValues();
        for (final MainValue mv : mvs) {
            if (mv.getMainValue().getId() == nmv.getId())
                return mv;
        }
        return null;
    }

    /**
     * Fetches a named main Q value from the database, if existing
     */
    private NamedMainValue fetchNamedQMainValue(final String name, final Session session) {
        final NamedMainValue nmv = NamedMainValue.fetchByNameAndType(name, MainValueTypeKey.Q.getName(), session);
        if (nmv != null)
            nmv.setOfficialLines(new ArrayList<OfficialLine>());
        return nmv;
    }

    /**
     * Fetches a named main Q(duration) value from the database, if existing
     */
    private NamedMainValue fetchNamedQMainValue(final int days, final Session session, final MainValueType qType) {
        final NamedMainValue nmv = NamedMainValue.fetchByNameAndType(Integer.toString(days), MainValueTypeKey.DURATION.getName(), session);
        // final NamedMainValue nmv = new NamedMainValue(Integer.toString(days), qType);
        if (nmv != null)
            nmv.setOfficialLines(new ArrayList<OfficialLine>());
        return nmv;
    }

    /**
     * Creates a main value for a main value name
     */
    private MainValue createMainValue(final Gauge gauge, final NamedMainValue nmv, final double value, final TimeInterval timeperiod) {
        return new MainValue(gauge, nmv, BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_EVEN), timeperiod); // TODO Scale per Formatter-Konstante o.ä.
    }

    /**
     * Gets the q from a list at a list position, or the next larger q if the immediate successors are equal
     */
    private double getDurationQ(final TDoubleArrayList qs, final int i) {
        if (i == 0)
            return qs.getQuick(0);
        for (int j = i; j <= qs.size() - 1; j++) {
            if (qs.getQuick(j) > qs.getQuick(j - 1) + 0.001)
                return qs.getQuick(j);
        }
        // Identical values at end of list: increment q on third significant digit
        final MathContext mc = new MathContext(3, RoundingMode.FLOOR);
        BigDecimal qplus = BigDecimal.valueOf(qs.getQuick(qs.size() - 1));
        qplus = qplus.round(mc).add(BigDecimal.ONE.scaleByPowerOfTen(-qplus.scale()));
        return qplus.doubleValue();
    }
}

http://dive4elements.wald.intevation.org