gernotbelger@9288: /* Copyright (C) 2011, 2012, 2013 by Bundesanstalt für Gewässerkunde gernotbelger@9288: * Software engineering by Intevation GmbH gernotbelger@9288: * gernotbelger@9288: * This file is Free Software under the GNU AGPL (>=v3) gernotbelger@9288: * and comes with ABSOLUTELY NO WARRANTY! Check out the gernotbelger@9288: * documentation coming with Dive4Elements River for details. gernotbelger@9288: */ gernotbelger@9288: gernotbelger@9288: package org.dive4elements.river.artifacts.services; gernotbelger@9288: gernotbelger@9288: import java.math.BigDecimal; mschaefer@9395: import java.math.MathContext; mschaefer@9392: import java.math.RoundingMode; gernotbelger@9288: import java.util.ArrayList; gernotbelger@9288: import java.util.Calendar; gernotbelger@9288: import java.util.Date; gernotbelger@9288: import java.util.List; gernotbelger@9288: gernotbelger@9288: import org.dive4elements.artifacts.CallMeta; gernotbelger@9288: import org.dive4elements.artifacts.GlobalContext; gernotbelger@9288: import org.dive4elements.artifacts.common.ArtifactNamespaceContext; mschaefer@9392: import org.dive4elements.artifacts.common.utils.DateUtils; gernotbelger@9288: import org.dive4elements.artifacts.common.utils.XMLUtils; mschaefer@9392: import org.dive4elements.river.backend.SessionHolder; gernotbelger@9288: import org.dive4elements.river.model.Gauge; gernotbelger@9288: import org.dive4elements.river.model.MainValue; gernotbelger@9288: import org.dive4elements.river.model.MainValueType; gernotbelger@9288: import org.dive4elements.river.model.MainValueType.MainValueTypeKey; gernotbelger@9288: import org.dive4elements.river.model.NamedMainValue; gernotbelger@9288: import org.dive4elements.river.model.OfficialLine; gernotbelger@9288: import org.dive4elements.river.model.River; gernotbelger@9288: import org.dive4elements.river.model.TimeInterval; mschaefer@9392: import org.dive4elements.river.model.sinfo.DailyDischargeValue; mschaefer@9392: import org.dive4elements.river.model.sinfo.DailyDischargeValue.OrderByField; mschaefer@9392: import org.dive4elements.river.utils.DoubleUtil; mschaefer@9392: import org.hibernate.Session; gernotbelger@9288: import org.w3c.dom.Document; gernotbelger@9288: mschaefer@9392: import gnu.trove.TDoubleArrayList; mschaefer@9392: gernotbelger@9288: /** gernotbelger@9288: * This service returns the main values of a river's gauge based on the start gernotbelger@9288: * and end point of the river. gernotbelger@9288: * gernotbelger@9288: * @author Ingo Weinzierl gernotbelger@9288: */ gernotbelger@9288: public class DynamicMainValuesService extends AbstractMainValuesService { gernotbelger@9288: gernotbelger@9288: private static final long serialVersionUID = 1L; gernotbelger@9288: gernotbelger@9288: private static final String XPATH_START_YEAR = "/art:mainvalues/art:startYear/text()"; gernotbelger@9288: gernotbelger@9288: private static final String XPATH_END_YEAR = "/art:mainvalues/art:endYear/text()"; gernotbelger@9288: gernotbelger@9288: @Override gernotbelger@9288: public Document doProcess(final Document data, final GlobalContext context, final CallMeta callMeta) { gernotbelger@9288: try { gernotbelger@9288: gernotbelger@9288: final River river = getRequestedRiver(data); gernotbelger@9288: final Gauge gauge = getRequestedGauge(data, river); gernotbelger@9288: final Date startTime = getRequestedStartYear(data); gernotbelger@9288: final Date endTime = getRequestedEndYear(data); gernotbelger@9288: gernotbelger@9288: final List mainValues = getMainValues(river, gauge, startTime, endTime); gernotbelger@9288: gernotbelger@9288: return buildDocument(river, gauge, mainValues, context); gernotbelger@9288: } gernotbelger@9288: catch (final MainValuesServiceException e) { mschaefer@9392: // e.printStackTrace(); mschaefer@9392: return error(e.getMessage()); mschaefer@9392: } mschaefer@9392: catch (final Exception e) { gernotbelger@9288: e.printStackTrace(); gernotbelger@9288: return error(e.getMessage()); gernotbelger@9288: } gernotbelger@9288: } gernotbelger@9288: gernotbelger@9288: private Date getRequestedStartYear(final Document data) throws MainValuesServiceException { gernotbelger@9288: gernotbelger@9288: final String startStr = XMLUtils.xpathString(data, XPATH_START_YEAR, ArtifactNamespaceContext.INSTANCE); gernotbelger@9288: gernotbelger@9288: if (startStr == null) gernotbelger@9288: throw new MainValuesServiceException("no start year"); gernotbelger@9288: gernotbelger@9288: try { gernotbelger@9288: final int year = Integer.parseInt(startStr); gernotbelger@9288: final Calendar cal = Calendar.getInstance(); gernotbelger@9288: cal.clear(); gernotbelger@9288: cal.set(year, 0, 1); gernotbelger@9288: return cal.getTime(); gernotbelger@9288: } gernotbelger@9288: catch (final NumberFormatException e) { gernotbelger@9288: e.printStackTrace(); gernotbelger@9288: throw new MainValuesServiceException("invalid start year"); gernotbelger@9288: } gernotbelger@9288: } gernotbelger@9288: gernotbelger@9288: private Date getRequestedEndYear(final Document data) throws MainValuesServiceException { gernotbelger@9288: gernotbelger@9288: final String endStr = XMLUtils.xpathString(data, XPATH_END_YEAR, ArtifactNamespaceContext.INSTANCE); gernotbelger@9288: gernotbelger@9288: if (endStr == null) gernotbelger@9288: throw new MainValuesServiceException("no end year"); gernotbelger@9288: gernotbelger@9288: try { gernotbelger@9288: final int year = Integer.parseInt(endStr); gernotbelger@9288: final Calendar cal = Calendar.getInstance(); gernotbelger@9288: cal.clear(); gernotbelger@9288: cal.set(year, 11, 31); gernotbelger@9288: return cal.getTime(); gernotbelger@9288: } gernotbelger@9288: catch (final NumberFormatException e) { gernotbelger@9288: e.printStackTrace(); gernotbelger@9288: throw new MainValuesServiceException("invalid end year"); gernotbelger@9288: } gernotbelger@9288: } gernotbelger@9288: gernotbelger@9288: /** mschaefer@9392: * Computes a gauge's main values for a period of time based on its daily discharges stored in the database gernotbelger@9288: */ mschaefer@9392: protected List getMainValues(final River river, final Gauge gauge, final Date startTime, final Date endTime) throws MainValuesServiceException { gernotbelger@9288: gernotbelger@9288: final List mainValues = new ArrayList<>(); mschaefer@9392: computeMainDischargeValues(gauge, startTime, endTime, mainValues); gernotbelger@9288: return mainValues; gernotbelger@9288: } mschaefer@9392: mschaefer@9392: /** mschaefer@9392: * Computes mnq, mq, mhq, hq5 and q(d=0..364) and adds them to a MainValue list mschaefer@9392: */ mschaefer@9392: private void computeMainDischargeValues(final Gauge gauge, final Date startTime, final Date endTime, final List mainValues) mschaefer@9392: throws MainValuesServiceException { mschaefer@9392: mschaefer@9392: // Query the gauge's daily Q values mschaefer@9392: final List qdvs = DailyDischargeValue.getValues(gauge, startTime, endTime, OrderByField.DAY); mschaefer@9392: if (qdvs.isEmpty()) mschaefer@9392: throw new MainValuesServiceException("no daily discharge values for gauge " + gauge.getName() + " in the requested time period"); mschaefer@9392: // return; // TODO Fehlermeldung mschaefer@9392: mschaefer@9392: // Build yearly aggregates mschaefer@9392: final TDoubleArrayList mnqs = new TDoubleArrayList(); mschaefer@9392: final TDoubleArrayList mqs = new TDoubleArrayList(); mschaefer@9392: int mqcnt = 0; mschaefer@9392: final TDoubleArrayList mhqs = new TDoubleArrayList(); mschaefer@9392: for (int i = 0, j = 0; i <= qdvs.size() - 1; i++) { mschaefer@9392: final DailyDischargeValue qdv = qdvs.get(i); mschaefer@9392: if ((i >= 1) && isSameQYear(qdv.getDay(), qdvs.get(i - 1).getDay())) { mschaefer@9392: // Continue aggregating the values of the current year mschaefer@9392: mnqs.set(j, Math.min(mnqs.get(j), qdv.getDischarge().doubleValue())); mschaefer@9392: mhqs.set(j, Math.max(mhqs.get(j), qdv.getDischarge().doubleValue())); mschaefer@9392: mqs.set(j, mqs.get(j) + qdv.getDischarge().doubleValue()); mschaefer@9392: mqcnt++; mschaefer@9392: if (i == qdvs.size() - 1) mschaefer@9392: mqs.set(j, mqs.get(j) / mqcnt); mschaefer@9392: } mschaefer@9392: else { mschaefer@9392: // Complete mq aggregation mschaefer@9392: if (mqcnt >= 1) { mschaefer@9392: mqs.set(j, mqs.get(j) / mqcnt); mschaefer@9392: j++; mschaefer@9392: } mschaefer@9392: // Start next year mschaefer@9392: mnqs.add(qdv.getDischarge().doubleValue()); mschaefer@9392: mhqs.add(qdv.getDischarge().doubleValue()); mschaefer@9392: mqs.add(qdv.getDischarge().doubleValue()); mschaefer@9392: mqcnt = 1; mschaefer@9392: } mschaefer@9392: } mschaefer@9392: mschaefer@9392: // Compute arithmetic means of the yearly values mschaefer@9392: final Session session = SessionHolder.HOLDER.get(); mschaefer@9392: final TimeInterval timeperiod = new TimeInterval(startTime, endTime); mschaefer@9392: final double mnq = DoubleUtil.sum(mnqs.toNativeArray()) / mnqs.size(); mschaefer@9392: mainValues.add(createMainValue(gauge, fetchNamedQMainValue("MNQ", session), mnq, timeperiod)); mschaefer@9392: final double mq = DoubleUtil.sum(mqs.toNativeArray()) / mqs.size(); mschaefer@9392: mainValues.add(createMainValue(gauge, fetchNamedQMainValue("MQ", session), mq, timeperiod)); mschaefer@9392: final double mhq = DoubleUtil.sum(mhqs.toNativeArray()) / mhqs.size(); mschaefer@9392: mainValues.add(createMainValue(gauge, fetchNamedQMainValue("MHQ", session), mhq, timeperiod)); mschaefer@9392: mschaefer@9395: // Compute hq5 - obsolete mschaefer@9395: // mhqs.sort(); mschaefer@9395: // final double hq5 = mhqs.get((int) Math.ceil(4 * mhqs.size() / 5)); mschaefer@9395: // mainValues.add(createMainValue(gauge, fetchNamedQMainValue("HQ5", session), hq5, timeperiod)); mschaefer@9395: mschaefer@9395: // Add HSQ-II from the gauge's main values mschaefer@9395: final MainValue hsq2 = fetchHsqII(gauge, session); mschaefer@9395: if (hsq2 != null) mschaefer@9395: mainValues.add(hsq2); mschaefer@9392: mschaefer@9392: // Query the gauge's daily Q values and build a list sorted by ascending Q mschaefer@9392: final TDoubleArrayList qs = new TDoubleArrayList(); mschaefer@9392: for (final DailyDischargeValue qdv : qdvs) mschaefer@9392: qs.add(qdv.getDischarge().doubleValue()); mschaefer@9392: qs.sort(); mschaefer@9392: mschaefer@9392: // Step through the sorted Q list and get the duration discharges mschaefer@9392: final int yearCnt = DateUtils.getYearFromDate(endTime) - DateUtils.getYearFromDate(startTime) + 1; mschaefer@9392: double glq20 = Double.NaN; mschaefer@9392: for (int i = 0, k = 0; (i <= 364) && (k <= qs.size() - 1); i++, k += yearCnt) { mschaefer@9392: final NamedMainValue nmv = fetchNamedQMainValue(i, session, mainValues.get(0).getMainValue().getType()); mschaefer@9395: if (nmv != null) { mschaefer@9395: final double q = getDurationQ(qs, k); mschaefer@9395: mainValues.add(createMainValue(gauge, nmv, q, timeperiod)); mschaefer@9395: if (i == 20) mschaefer@9395: glq20 = q; mschaefer@9395: } mschaefer@9392: } mschaefer@9392: mainValues.add(createMainValue(gauge, fetchNamedQMainValue("GlQ", session), glq20, timeperiod)); mschaefer@9392: } mschaefer@9392: mschaefer@9392: /** mschaefer@9392: * Checks year equality of two dates (calendar year) mschaefer@9392: */ mschaefer@9392: private boolean isSameQYear(final Date a, final Date b) { mschaefer@9392: final Calendar ca = Calendar.getInstance(); mschaefer@9392: ca.setTime(a); mschaefer@9392: final Calendar cb = Calendar.getInstance(); mschaefer@9392: cb.setTime(b); mschaefer@9392: return (ca.get(Calendar.YEAR) == cb.get(Calendar.YEAR)); mschaefer@9392: } mschaefer@9392: mschaefer@9392: /** mschaefer@9395: * Fetches the gauge's HSQ-II from the database, or returns null mschaefer@9395: */ mschaefer@9395: private MainValue fetchHsqII(final Gauge gauge, final Session session) { mschaefer@9395: final NamedMainValue nmv = NamedMainValue.fetchByNameAndType("HSQ-II", MainValueTypeKey.UNKNOWN.getName(), session); mschaefer@9395: if (nmv == null) mschaefer@9395: return null; mschaefer@9395: final List mvs = gauge.getMainValues(); mschaefer@9395: for (final MainValue mv : mvs) { mschaefer@9395: if (mv.getMainValue().getId() == nmv.getId()) mschaefer@9395: return mv; mschaefer@9395: } mschaefer@9395: return null; mschaefer@9395: } mschaefer@9395: mschaefer@9395: /** mschaefer@9392: * Fetches a named main Q value from the database, if existing mschaefer@9392: */ mschaefer@9392: private NamedMainValue fetchNamedQMainValue(final String name, final Session session) { mschaefer@9392: final NamedMainValue nmv = NamedMainValue.fetchByNameAndType(name, MainValueTypeKey.Q.getName(), session); mschaefer@9392: if (nmv != null) mschaefer@9392: nmv.setOfficialLines(new ArrayList()); mschaefer@9392: return nmv; mschaefer@9392: } mschaefer@9392: mschaefer@9392: /** mschaefer@9392: * Fetches a named main Q(duration) value from the database, if existing mschaefer@9392: */ mschaefer@9392: private NamedMainValue fetchNamedQMainValue(final int days, final Session session, final MainValueType qType) { mschaefer@9392: final NamedMainValue nmv = NamedMainValue.fetchByNameAndType(Integer.toString(days), MainValueTypeKey.DURATION.getName(), session); mschaefer@9392: // final NamedMainValue nmv = new NamedMainValue(Integer.toString(days), qType); mschaefer@9392: if (nmv != null) mschaefer@9392: nmv.setOfficialLines(new ArrayList()); mschaefer@9392: return nmv; mschaefer@9392: } mschaefer@9392: mschaefer@9392: /** mschaefer@9392: * Creates a main value for a main value name mschaefer@9392: */ mschaefer@9392: private MainValue createMainValue(final Gauge gauge, final NamedMainValue nmv, final double value, final TimeInterval timeperiod) { mschaefer@9392: return new MainValue(gauge, nmv, BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_EVEN), timeperiod); // TODO Scale per Formatter-Konstante o.ä. mschaefer@9392: } mschaefer@9392: mschaefer@9392: /** mschaefer@9392: * Gets the q from a list at a list position, or the next larger q if the immediate successors are equal mschaefer@9392: */ mschaefer@9392: private double getDurationQ(final TDoubleArrayList qs, final int i) { mschaefer@9395: if (i == 0) mschaefer@9395: return qs.getQuick(0); mschaefer@9395: for (int j = i; j <= qs.size() - 1; j++) { mschaefer@9395: if (qs.getQuick(j) > qs.getQuick(j - 1) + 0.001) mschaefer@9395: return qs.getQuick(j); mschaefer@9395: } mschaefer@9395: // Identical values at end of list: increment q on third significant digit mschaefer@9395: final MathContext mc = new MathContext(3, RoundingMode.FLOOR); mschaefer@9395: BigDecimal qplus = BigDecimal.valueOf(qs.getQuick(qs.size() - 1)); mschaefer@9395: qplus = qplus.round(mc).add(BigDecimal.ONE.scaleByPowerOfTen(-qplus.scale())); mschaefer@9395: return qplus.doubleValue(); mschaefer@9392: } gernotbelger@9288: }