Skip to content
Snippets Groups Projects
WeatherUtil.java 62.75 KiB
/*
 * Copyright (c) 2015 NIBIO <http://www.nibio.no/>. 
 * 
 * This file is part of VIPSCommon.
 * VIPSCommon is free software: you can redistribute it and/or modify
 * it under the terms of the NIBIO Open Source License as published by 
 * NIBIO, either version 1 of the License, or (at your option) any
 * later version.
 * 
 * VIPSCommon is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * NIBIO Open Source License for more details.
 * 
 * You should have received a copy of the NIBIO Open Source License
 * along with VIPSCommon.  If not, see <http://www.nibio.no/licenses/>.
 * 
 */

package no.nibio.vips.util;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.TimeZone;
import no.nibio.vips.entity.WeatherObservation;
import no.nibio.vips.model.ConfigValidationException;

/**
 * Weather related utility methods
 * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
 */
public class WeatherUtil {
    
    public final static int AGGREGATION_TYPE_AVERAGE = 1;
    public final static int AGGREGATION_TYPE_SUM = 2;
    public final static int AGGREGATION_TYPE_MINIMUM = 3;
    public final static int AGGREGATION_TYPE_MAXIMUM = 4;
    public final static int AGGREGATION_TYPE_SUM_GLOBAL_RADIATION = 5;

    /**
     * Ensures that hourly values at the end of series that exceed
     * the day before is cut of
     * @param hourlyValues
     * @return 
     */
    public List<WeatherObservation> cutTrailingHourlyValues(List<WeatherObservation> hourlyValues, TimeZone timeZone) {
        // Special case: List has less than 24 hourly values, return empty list
        if(hourlyValues.size() < 24)
        {
            return new ArrayList<WeatherObservation>();
        }
        // Sort the list 
        Collections.sort(hourlyValues);
        // Get the last 24 values, see if they're in the same day
        Integer dateShiftIndex = 0;
        for(int i=1;i<24;i++)
        {
            if(!this.isSameDate(hourlyValues.get(hourlyValues.size()-i),hourlyValues.get(hourlyValues.size()-i-1),timeZone))
            {
                dateShiftIndex = hourlyValues.size()-i;
                break;
            }
        }
        if(dateShiftIndex == 0)
        {
            return hourlyValues;
        }
        else
        {
            return new ArrayList<WeatherObservation>(hourlyValues.subList(0,dateShiftIndex));
        }
    }

    /**
     * 
     * @param obs1
     * @param obs2
     * @param timeZone
     * @return true if the two dates are the same date, given the time zone
     */
    private boolean isSameDate(WeatherObservation obs1, WeatherObservation obs2, TimeZone timeZone) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        format.setTimeZone(timeZone);
        return format.format(obs1.getTimeMeasured()).equals(format.format(obs2.getTimeMeasured()));
    }

    /**
     * 
     * @param temperature
     * @param relativeHumidity
     * @return 
     */
    public List<WeatherObservation> calculateLeafWetnessHourSeriesSimple(List<WeatherObservation> temperature, List<WeatherObservation> rainfall, List<WeatherObservation> relativeHumidity) throws ConfigValidationException
    {
        // Just to be sure!
        Collections.sort(temperature);
        Collections.sort(rainfall);
        Collections.sort(relativeHumidity);
        
        List<WeatherObservation> retVal = new ArrayList<WeatherObservation>();
        Iterator<WeatherObservation> humIt = relativeHumidity.iterator();
        WeatherObservation humObs;
        Iterator<WeatherObservation> rainIt = rainfall.iterator();
        WeatherObservation rainObs;
        
        for(WeatherObservation tempObs:temperature)
        {
            if(humIt.hasNext() && rainIt.hasNext())
            {
                humObs = humIt.next();
                rainObs = rainIt.next();
            }
            else
            {
                throw new ConfigValidationException("When calculating leaf wetness: "
                        + "Differing lengths of lists with input weather data. "
                        + "Temperature list has " + temperature.size() + " elements, "
                        + "rainfall list has " + rainfall.size() + " elements, "
                        + "relative humidity list has " + relativeHumidity.size() + " elements.");
            }
            //System.out.println("humObs timestamp=" + humObs.getTimeMeasured());
            if(humObs.getTimeMeasured().compareTo(tempObs.getTimeMeasured()) != 0 || humObs.getTimeMeasured().compareTo(rainObs.getTimeMeasured()) != 0)
            {
                throw new ConfigValidationException("When calculating leaf wetness: "
                        + "Timestamps of weather observations are not parallel. "
                        + "Temperature has timestamp " + tempObs.getTimeMeasured() + ", "
                        + "rainfall has timestamp " + rainObs.getTimeMeasured() + ", "
                        + "relative humidity has timestamp " + humObs.getTimeMeasured() + ".");
            }
            
            WeatherObservation leafWetness = new WeatherObservation();
            leafWetness.setElementMeasurementTypeId(WeatherElements.LEAF_WETNESS);
            leafWetness.setLogIntervalId(WeatherObservation.LOG_INTERVAL_ID_1H);
            leafWetness.setTimeMeasured(humObs.getTimeMeasured());
            leafWetness.setValue(this.calculateLeafWetnessHourSimple(tempObs.getValue(), rainObs.getValue(), humObs.getValue()));
            retVal.add(leafWetness);
        }
        return retVal;
    }

    /**
     * Calculate leaf wetness from a limited set of weather parameters.
     * Based on the method used by Marc Trapman in RIMPro (version 2014)
     * @param temperature degrees Celcius, average for 1 hour
     * @param rainfall millimeters, aggregate for 1 hour
     * @param relativeHumidity percent, average for 1 hour
     * @return minutes with leaf wetness during one hour. In this simple method,
     * it returns either 60 or 0 (all or nothing!)
     */
    public Double calculateLeafWetnessHourSimple(Double temperature, Double rainfall, Double relativeHumidity)
    {
        return (rainfall > 0 || this.getVaporPressureDeficit(temperature, relativeHumidity) < 2) ? 60d : 0;
    }
    
    /**
     * This is after Bolton 1980
     * @param relativeHumidity
     * @param temperature
     * @return Vapor Pressure Deficit in kPa (kiloPascal)
     */
    public double getVaporPressureDeficit(Double temperature, Double relativeHumidity)
    {
        return ((100-relativeHumidity) / 100) * 6.112 * Math.exp(17.67 * temperature / (temperature + 243.5));
    }
    
    /**
     * Calculates Water Vapor Deficiency in Pa
     * @param temperature
     * @param relative humidity
     * @return Water Vapor Deficiency in Pa
     */
    public double getWVD(double temperature, double relativeHumidity) {
        double saturationPressure = this.getSaturationPressure(temperature);
        double partialPressure = this.getPartialPressure(saturationPressure, relativeHumidity);
        /*
        if(this.DEBUG)
        System.out.println("[BlightModelActivity] DEBUG: saturationPressure=" + saturationPressure + ", partialPressure=" + partialPressure + ", WVD=" + ((saturationPressure - partialPressure) * 1000));
         * */
        return (saturationPressure - partialPressure) * 1000;
    }

    /**
     * Calculates the partial pressure
     * @param saturationPressure in kPa
     * @param RH
     * @return partial pressure in kPa
     */
    public double getPartialPressure(double saturationPressure, double relativeHumidity) {
        return relativeHumidity * saturationPressure / 100;
    }

    /**
     * Calculates the saturation pressure, based on temperature
     * @param temperature
     * @return saturation pressure in kPa
     */
    public double getSaturationPressure(double temperature) {
        return 0.61078 * Math.exp(17.269 * temperature / (temperature + 237.3));
    }
    
    /**
     * Aggregates daily values. No tolerance for duplicates
     * @param observations
     * @param timeZone
     * @param minimumObservationsPerDay
     * @param typeOfAggregation
     * @return
     * @throws WeatherObservationListException
     * @throws InvalidAggregationTypeException 
     */
    public List<WeatherObservation> getAggregatedDailyValues(
            List<WeatherObservation> observations, TimeZone timeZone, Integer minimumObservationsPerDay, Integer typeOfAggregation) 
            throws WeatherObservationListException,
            InvalidAggregationTypeException
    {
        return this.getAggregatedDailyValues(observations, timeZone, minimumObservationsPerDay, typeOfAggregation, 0);
    }
    
    /**
     * Aggregates daily values. Can be set to tolerate duplicates
     * @param observations the value of observations
     * @param timeZone the value of timeZone
     * @param minimumObservationsPerDay the value of minimumObservationsPerDay
     * @param typeOfAggregation the value of typeOfAggregation
     * @param maxDuplicates the value of maxDuplicates
     * @return the java.util.List<no.nibio.vips.entity.WeatherObservation>
     */
    public List<WeatherObservation> getAggregatedDailyValues(
            List<WeatherObservation> observations, TimeZone timeZone, Integer minimumObservationsPerDay, Integer typeOfAggregation, Integer maxDuplicates) 
            throws WeatherObservationListException,
            InvalidAggregationTypeException
    {
        if(observations == null || observations.isEmpty())
        {
            return null;
        }
        // First we organize the hourly values into one bucket per day
        Map<Date,Map> dateBucket = new HashMap<Date,Map>();
        String expectedParameter = observations.get(0).getElementMeasurementTypeId();
        Date lastDate = null;
        Integer numberOfDuplicates = 0;
        List<Date> duplicateTimestamps = new ArrayList<>();
        for(WeatherObservation observation:observations)
        {
            if(!observation.getElementMeasurementTypeId().equals(expectedParameter))
            {
                throw new WeatherObservationListException("Found multiple parameters: " + observation.getElementMeasurementTypeId() + " and " + expectedParameter);
            }
            Date theDate = normalizeToExactDate(observation.getTimeMeasured(), timeZone);
            lastDate = lastDate == null ? theDate : (lastDate.compareTo(theDate) < 0 ? theDate : lastDate);
            Map<Date, Double> hourValuesForDate = dateBucket.get(theDate);
            if(hourValuesForDate == null)
            {
                hourValuesForDate = new HashMap<Date,Double>();
                dateBucket.put(theDate, hourValuesForDate);
            }
            
            // Check for double entries
            // If found, we have no idea what to do with it. For now we discard it
            Double possibleDuplicate = hourValuesForDate.get(observation.getTimeMeasured());
            if(possibleDuplicate != null)
            {
                numberOfDuplicates++;
                duplicateTimestamps.add(observation.getTimeMeasured());
                if(numberOfDuplicates > maxDuplicates)
                {
                    String errorMessage =  "Found duplicate weatherObservations for parameter " +
                            observation.getElementMeasurementTypeId() + " at time(s) ";
                    for(Date duplicateTimestamp : duplicateTimestamps)
                    {
                        errorMessage += duplicateTimestamp.toString() + ", ";
                    }
                    throw new WeatherObservationListException(errorMessage);
                }
                continue;
            }
            
            hourValuesForDate.put(observation.getTimeMeasured(), observation.getValue());
        }
        
        // Then we iterate the buckets, do the aggregation and create return values
        List<WeatherObservation> aggregatedObservations = new ArrayList<WeatherObservation>();
        WeatherObservation templateObservation = observations.get(0);
        Double aggregateValue;
        for(Date aDay:dateBucket.keySet())
        {
            //System.out.println("date=" + aDay);
            Map hourValuesForADay = dateBucket.get(aDay);
            // We accept less than minimum values for the last day (we don't throw an error)
            if(hourValuesForADay.size() < minimumObservationsPerDay && aDay.compareTo(lastDate) < 0)
            {
                throw new WeatherObservationListException(
                        "Too few observations to aggregate for parameter " +
                        templateObservation.getElementMeasurementTypeId() +
                        " at date " + aDay +". Found " + hourValuesForADay.size() +
                        ", expected minimum " + minimumObservationsPerDay
                        );
            }
            // If last day and too few values: Skip it
            else if(hourValuesForADay.size() < minimumObservationsPerDay)
            {
                continue;
            }
            switch(typeOfAggregation){
                case WeatherUtil.AGGREGATION_TYPE_AVERAGE:
                    aggregateValue = getAverage(hourValuesForADay.values()); break;
                case WeatherUtil.AGGREGATION_TYPE_SUM:
                    aggregateValue = getSum(hourValuesForADay.values()); break;
                case WeatherUtil.AGGREGATION_TYPE_MINIMUM:
                    aggregateValue = getMinimum(hourValuesForADay.values()); break;
                case WeatherUtil.AGGREGATION_TYPE_MAXIMUM:
                    aggregateValue = getMaximum(hourValuesForADay.values()); break;
                case WeatherUtil.AGGREGATION_TYPE_SUM_GLOBAL_RADIATION:
                    aggregateValue = getSum(hourValuesForADay.values()) * 0.0036; break;
                default:
                    throw new InvalidAggregationTypeException(
                            "No aggregation method with id= " + typeOfAggregation  + " exists."
                            );
            }
            WeatherObservation aggregatedObservation = new WeatherObservation();
            aggregatedObservation.setElementMeasurementTypeId(templateObservation.getElementMeasurementTypeId());
            aggregatedObservation.setLogIntervalId(WeatherObservation.LOG_INTERVAL_ID_1D);
            aggregatedObservation.setTimeMeasured(aDay);
            aggregatedObservation.setValue(aggregateValue);
            aggregatedObservations.add(aggregatedObservation);
        }
        return aggregatedObservations;
    }
    
    public List<Double> getObservationValues(List<WeatherObservation> observations)
    {
        List<Double> values = new ArrayList<>();
        for(WeatherObservation obs:observations)
        {
            values.add(obs.getValue());
        }
        
        return values;
    }
    
    public Double getAverage(Collection<Double> values)
    {
        return this.getSum(values)/values.size();
    }

    public Double getSum(Collection<Double> values)
    {
       Double sum = 0d;
        for(Double value:values)
        {
            sum += value;
        }
        return sum;
    }
    
    public Double getMinimum(Collection<Double> values)
    {
        Double minimum = null;
        for(Double value:values)
        {
            minimum = minimum == null ? value : Math.min(minimum, value);
        }
        return minimum;
    }
    
    public Double getMaximum(Collection<Double> values)
    {
        Double maximum = null;
        for(Double value:values)
        {
            maximum = maximum == null ? value : Math.max(maximum, value);
        }
        return maximum;
    }
    
    /**
     * Sets hours, minutes, seconds and milliseconds to ZERO
     * @param timeStamp
     * @param timeZone
     * @return 
     */
    public Date normalizeToExactDate(Date timeStamp, TimeZone timeZone)
    {
        Calendar cal = Calendar.getInstance(timeZone);
        cal.setTime(timeStamp);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND,0);
        cal.set(Calendar.MILLISECOND,0);
        return cal.getTime();
    }
    
    /**
     * Slicing off minutes, seconds and milliseconds
     * @param timeStamp
     * @param timeZone
     * @return 
     */
     public Date normalizeToExactHour(Date timeStamp, TimeZone timeZone)
    {
        Calendar cal = Calendar.getInstance(timeZone);
        cal.setTime(timeStamp);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND,0);
        cal.set(Calendar.MILLISECOND,0);
        return cal.getTime();
    }
    
    /**
     * Adjusts the date so that it is a Date with the same time (hours, minutes etc.) in the new timezone
     * @param theDate
     * @param timeZone
     * @return 
     */
    public Date changeDateTimeZone(Date theDate, TimeZone oldTimeZone, TimeZone newTimeZone)
    {
        Calendar oldCal = Calendar.getInstance(oldTimeZone);
        Calendar newCal = Calendar.getInstance(newTimeZone);
        
        oldCal.setTime(theDate);
        newCal.set(oldCal.get(Calendar.YEAR), 
                oldCal.get(Calendar.MONTH), 
                oldCal.get(Calendar.DATE), 
                oldCal.get(Calendar.HOUR_OF_DAY), 
                oldCal.get(Calendar.MINUTE), 
                oldCal.get(Calendar.SECOND)
        );
        newCal.set(Calendar.MILLISECOND, oldCal.get(Calendar.MILLISECOND));
        return newCal.getTime();
    }
    
    /**
     * Assuming that this is a midnight timestamp that has been skewed 
     * due to DST changes, the method attempts to return a correct midnight
     * time stamp in the requested time zone
     * @param timeStamp
     * @param timeZone
     * @return 
     */
    public Date pragmaticAdjustmentToMidnight(Date timeStamp, TimeZone timeZone)
    {
        Calendar cal = Calendar.getInstance(timeZone);
        cal.setTime(timeStamp);
        // If we're close to BEFORE midnight, add one day
        if(cal.get(Calendar.HOUR_OF_DAY) >= 22)
        {
            cal.add(Calendar.DATE, 1);
        }
        // Then set hours, minutes etc. to zero
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND,0);
        cal.set(Calendar.MILLISECOND,0);
        return cal.getTime();
    }

    /**
     * Given input data, attempts to calculate leaf wetness. Does not overwrite provided leaf wetness data.
     * Priority (depending on what data are provided):
     * <ol>
     * <li>Provided leaf wetness data</li>
     * <li>Nærstad's leaf wetness calculation algorithm</li>
     * <li>Trapman's simple leaf wetness calculation algorithm</li>
     * </ol>
     * <p>Requires that at least TM, RR, and UM are complete. And that all lists are sorted!</p>
     * 
     * @param BT Leaf wetness (minutes/hour)
     * @param TM Mean temperature (hourly mean, degrees Celcius)
     * @param RR Rainfall (mm, hourly aggregate)
     * @param FM2 Wind speed at 2m height (average, m/s)
     * @param Q0 Global radiation (unit??)
     * @param UM Relative humidity (average, %)
     * @return 
     */
    public List<WeatherObservation> calculateLeafWetnessHourSeriesBestEffort(List<WeatherObservation> BT, List<WeatherObservation> TM, List<WeatherObservation> RR, List<WeatherObservation> Q0, List<WeatherObservation> FM2, List<WeatherObservation> UM) throws ConfigValidationException {
        // If BT is same length as TM, return BT unmodified
        if(BT.size() == TM.size())
        {
            return BT;
        }
        
        // Possible TODO: Validate that TM==RR==UM
        // First: Find time and index for when to start calculating
        
        Date startTime, lastBTTime;
        if(BT.size() > 0)
        {
            Collections.sort(BT);
            lastBTTime = BT.get(BT.size()-1).getTimeMeasured();
            Calendar cal = Calendar.getInstance();
            cal.setTime(lastBTTime);
            cal.add(Calendar.HOUR_OF_DAY, 1);
            startTime = cal.getTime();
        }
        else
        {
            startTime = TM.get(0).getTimeMeasured();
            lastBTTime = null;
        }
        
        
        
        // If all parameters are complete, we can use Nærstad's algorithm. Otherwise, if we only have UM,
        // we must resort to Trapman's algorithm
        List<WeatherObservation> calculatedBT;
        if(TM.size() == RR.size() && TM.size() == Q0.size() && TM.size() == FM2.size() && TM.size() == UM.size())
        {
            // The algorithm uses the first hour of input values as "startup"
            // So if we have some leaf wetness, we can provide the first hour.
            if(BT.size() > 0)
            {
                //System.out.println("Nærstad BT with BT measured values: startTime=" + startTime);
                //System.out.println("Last BT timestamp = " + BT.get(BT.size()-1).getTimeMeasured());
                calculatedBT = this.calculateLeafWetnessHourSeriesNaerstad(
                        this.getWeatherObservationsInPeriod(TM, lastBTTime, null),
                        this.getWeatherObservationsInPeriod(RR, lastBTTime, null),
                        this.getWeatherObservationsInPeriod(Q0, lastBTTime, null),
                        this.getWeatherObservationsInPeriod(FM2, lastBTTime, null),
                        this.getWeatherObservationsInPeriod(UM, lastBTTime, null),
                        BT.get(BT.size()-1)
                );
            }
            // Otherwise, we have to create a dry hour as the first hour
            else
            {
                //System.out.println("Nærstad BT without BT measured values: startTime=" + startTime);
                WeatherObservation emptyObs = new WeatherObservation(
                                                startTime,
                                                WeatherElements.LEAF_WETNESS,
                                                WeatherObservation.LOG_INTERVAL_ID_1H,
                                                0d
                                                );
                calculatedBT = new ArrayList<WeatherObservation>();
                calculatedBT.add(emptyObs);
                calculatedBT.addAll(this.calculateLeafWetnessHourSeriesNaerstad(TM, RR, Q0, FM2, UM, null));
            }
        }
        else if(TM.size() == RR.size() && TM.size() == UM.size())
        {
            /*
            System.out.println("Trapman BT: startTime=" + startTime);
            if(BT.size() > 0)
            {
                System.out.println("Last BT timestamp = " + BT.get(BT.size()-1).getTimeMeasured());
            }
            */
            calculatedBT = this.calculateLeafWetnessHourSeriesSimple(
                    this.getWeatherObservationsInPeriod(TM, startTime, null),
                    this.getWeatherObservationsInPeriod(RR, startTime, null),
                    this.getWeatherObservationsInPeriod(UM, startTime, null)
            );
        }
        else
        {
            throw new ConfigValidationException("Missing weather data. Number of _found_ observations per parameter (deviating number indicates missing data for that parameter): "
                    + "TM=" + TM.size()
                    + ",RR=" + RR.size()
                    + ",UM=" + UM.size()
            );
        }
        //System.out.println("First new obs timestamp = " + calculatedBT.get(0).getTimeMeasured());
        BT.addAll(calculatedBT);
        return BT;
    }
    
    /**
     * Extract all observations in the given period from given list
     * @param allObservations
     * @param dateStart INCLUDING start date. May be null 
     * @param dateEnd INCLUDING end time. May be null (if both dateStart AND dateEnd == null, the list is the same as the one sent in)
     * @return 
     */
    public List<WeatherObservation> getWeatherObservationsInPeriod(List<WeatherObservation> allObservations, Date dateStart, Date dateEnd)
    {
        List<WeatherObservation> retVal = new ArrayList<WeatherObservation>();
        for(WeatherObservation obs:allObservations)
        {
            if(
                    (dateStart == null || obs.getTimeMeasured().compareTo(dateStart) >= 0) 
                    && (dateEnd == null || obs.getTimeMeasured().compareTo(dateEnd) <= 0)
                    )
            {
                retVal.add(obs);
            }
        }
        return retVal;
    }

    /**
     * 
     * @param temperature
     * @param precipitation
     * @param globalRadiation
     * @param wind2m
     * @param relativeHumidity
     * @param precedingLeafWetness
     * @return Leaf wetness for the same time period as provided by input data, 
     * EXCEPT for the first hour, as that is used for "startup" of the algorithm.
     * So the size of the returned list is 1 less than the input lists.
     */
    private List<WeatherObservation> calculateLeafWetnessHourSeriesNaerstad(
            List<WeatherObservation> temperature, 
            List<WeatherObservation> precipitation, 
            List<WeatherObservation> globalRadiation, 
            List<WeatherObservation> wind2m, 
            List<WeatherObservation> relativeHumidity,
            WeatherObservation precedingLeafWetness) 
    {
        // TODO Validation
        
        List<WeatherObservation> calculatedLeafWetnessSeries = new ArrayList<WeatherObservation>();
        Double lastLatentHeatFlux = this.calculateLatentHeatFlux(
                temperature.get(0).getValue(), 
                relativeHumidity.get(0).getValue(), 
                wind2m.get(0).getValue(), 
                globalRadiation.get(0).getValue()
        );
        Integer precedingLeafWetnessValue = precedingLeafWetness != null ?  (int) precedingLeafWetness.getValue() : 0;
        // Iterating list with index (instead of using Iterator pattern) has 
        // performance gains on very long lists
        for(int i=1;i<temperature.size();i++)
        {
            WeatherObservation calculatedLeafWetness = new WeatherObservation(
                    temperature.get(i).getTimeMeasured(),
                    WeatherElements.LEAF_WETNESS,
                    WeatherObservation.LOG_INTERVAL_ID_1H,
                    (double) this.calculateLeafWetnessNaerstad(lastLatentHeatFlux, precedingLeafWetnessValue, precipitation.get(i).getValue())
            );
            calculatedLeafWetnessSeries.add(calculatedLeafWetness);
        }
        return calculatedLeafWetnessSeries;
    }
    
    /**
     * Nærstad's model of leaf wetness calculation
     * @param lastLatentHeatFlux
     * @param lastLeafWetness
     * @param precipitation
     * @return
     */
    public Long calculateLeafWetnessNaerstad(Double lastLatentHeatFlux, Integer lastLeafWetness, Double precipitation) {

        Long DLeafWetness = Math.round(lastLeafWetness - lastLatentHeatFlux);
        if (DLeafWetness < 0) {
            DLeafWetness = 0l;
        } else if (DLeafWetness > 60) {
            DLeafWetness = 60l;
        }


        Long leafWetness = Math.round(DLeafWetness + precipitation * 150);
        if (leafWetness < 0) {
            leafWetness = 0l;
        } else if (leafWetness > 60) {
            leafWetness = 60l;
        }

        return leafWetness;
    }
    
    /**
     * Part of the Nærstad leaf wetness calculation
     * @return
     */
    public Double calculateLatentHeatFlux(Double temperature, Double relativeHumidity, Double wind2m, Double globalRadiation) {
        // s(t) = 0.61078*EKSP(17.269* T(t)/( T(t)+237.3))*4.097*10000/(T(t)+237.3)2
        Double s = 0.61078 * Math.exp(17.269 * temperature / (temperature + 237.3)) * 4.097 * 10000 / Math.pow(temperature + 237.3, 2);
        // resistance(t) = 307*(0.07/( (windFF(t)   + 0.1) / 2 ))0.5
        Double resistance = 307 * Math.pow(0.07 / ((wind2m + 0.1) / 2), 0.5);
        // LE(t)  = -( (s(t) * Rn(t)  + (12(WVD (t) )/( resistance(t)))/(s(t) +0.64)
        Double LE = (s * globalRadiation + (12 * this.getWVD(temperature, relativeHumidity) / resistance)) / (s + 0.64);

        return LE;
    }

    /**
     * Fixes by 
     * <ul>
     * <li>Simple interpolation for continuous parameters (temperature, humidity)</li>
     * <li>Setting values to 0 for non continuous parameters (rainfall)</li>
     * </ul>
     * Maximum number of missing values in hole: 3 (hours)
     * @param obsList list of observations of same type (parameter)
     * @return 
     */
    public List<WeatherObservation> checkForAndFixHourlyTimeSeriesHoles(List<WeatherObservation> obsList) throws WeatherObservationListException {
        if(obsList == null || obsList.isEmpty())
        {
            return obsList;
        }
        //System.out.println("Number of observations before fix = " + obsList.size());
        // First we sort the list
        Collections.sort(obsList);
        Date currentTime = obsList.get(0).getTimeMeasured();
        Calendar cal = Calendar.getInstance();
        cal.setTime(currentTime);
        //System.out.println("checkForAndFixHourlyTimeSeriesHoles " + obsList.get(0).getElementMeasurementTypeId() + ",start time= " + currentTime );
        for(int i=0;i<obsList.size();i++)
        {
            
            /*WeatherObservation debugObs = obsList.get(i);
            if(debugObs.getElementMeasurementTypeId().equals("UM"))
            {
                System.out.println(debugObs.getTimeMeasured() + ": " + debugObs.getValue() + ", currentTime=" + currentTime);
            }*/
            
            int missingValues = 0;
            while(obsList.get(i).getTimeMeasured().compareTo(currentTime) != 0 
                    && obsList.get(i).getTimeMeasured().compareTo(currentTime) > 0)
            {
                missingValues++;
                // We have a hole
                //System.out.println("Hole found! For " + obsList.get(i).getElementMeasurementTypeId() + " at " + currentTime);
                
                // Must advance time until we find a matching time
                cal.add(Calendar.HOUR_OF_DAY, 1);
                currentTime = cal.getTime();
            }
            //System.out.println("Missing values=" + missingValues);
            if(missingValues > 3)
            {
                throw new WeatherObservationListException("More than three missing values for "
                        + "parameter " + obsList.get(0).getElementMeasurementTypeId() 
                        + " between " + obsList.get(i-1).getTimeMeasured() + " and "
                        + obsList.get(i).getTimeMeasured() + ". Can't fix this!");
            }
            else if(missingValues > 0)
            {
                List<WeatherObservation> calculatedValues = new ArrayList<WeatherObservation>();
                String elementMeasurementTypeId = obsList.get(i).getElementMeasurementTypeId();
                String fixingStrategy = this.getFixingStrategy(elementMeasurementTypeId);
                Date lastTimestampBeforeHole = obsList.get(i-1).getTimeMeasured();
                Calendar cal2 = Calendar.getInstance();
                cal2.setTime(lastTimestampBeforeHole);
                cal2.add(Calendar.HOUR_OF_DAY, 1);
                if(fixingStrategy.equals(WeatherElements.FIXING_STRATEGY_INTERPOLATE))
                {
                    Double lastValueBeforeHole = obsList.get(i-1).getValue();
                    Double valueDifference = obsList.get(i).getValue() - lastValueBeforeHole;
                    Double step = valueDifference / (missingValues + 1);
                    
                    for(int j=1;j<=missingValues;j++)
                    {
                        Double calculatedValue = lastValueBeforeHole + (step * j);
                        //System.out.println("CalculatedValue=" + calculatedValue);
                        WeatherObservation calculatedObservation = new WeatherObservation(
                                cal2.getTime(), 
                                elementMeasurementTypeId,
                                WeatherObservation.LOG_INTERVAL_ID_1H,
                                calculatedValue
                        );
                        //System.out.println(calculatedObservation);
                        cal2.add(Calendar.HOUR_OF_DAY, 1);
                        calculatedValues.add(calculatedObservation);
                    }
                }
                else if(fixingStrategy.equals(WeatherElements.FIXING_STRATEGY_SET_ZERO))
                {
                    for(int j=1;j<=missingValues;j++)
                    {
                        //System.out.println("CalculatedValue=" + calculatedValue);
                        WeatherObservation calculatedObservation = new WeatherObservation(
                                cal2.getTime(), // TODO!!!
                                elementMeasurementTypeId,
                                WeatherObservation.LOG_INTERVAL_ID_1H,
                                0d
                        );
                        cal2.add(Calendar.HOUR_OF_DAY, 1);
                        calculatedValues.add(calculatedObservation);
                    }
                }
                obsList.addAll(i, calculatedValues);
                i = i + missingValues;
            }
            
            // Advance time, move on
            cal.add(Calendar.HOUR, 1);
            currentTime = cal.getTime();
        }
        //System.out.println("Number of observations after fix = " + obsList.size());
        return obsList;
    }
    
    public String getFixingStrategy(String elementMeasurementCode)
    {
        return elementMeasurementCode.equals(WeatherElements.PRECIPITATION) ? WeatherElements.FIXING_STRATEGY_SET_ZERO : WeatherElements.FIXING_STRATEGY_INTERPOLATE;
    }
    
    /**
     * Attempts to return a list with complete series in same period for different parameters
     * @param mixedParameterList
     * @param parameterNames 
     * @return 
     */
    public List<WeatherObservation> fixHourlyValuesForParameters(List<WeatherObservation> mixedParameterList, Set parameterNames, Date firstTimestamp, Date lastTimestamp) throws WeatherObservationListException
    {
        // Removing duplicates first 
        mixedParameterList = this.removeDuplicateWeatherObservations(mixedParameterList, null);
        
        Map<String, List<WeatherObservation>> separatedParameters = new HashMap<String, List<WeatherObservation>>();
        
        Date estimatedLastTimestamp = lastTimestamp;
        // Separating the parameters
        for(WeatherObservation obs : mixedParameterList)
        {
            // Skip the irrelevant observations
            // Check for parameter name and timestamp
            if( 
                    !parameterNames.contains(obs.getElementMeasurementTypeId())
                    || obs.getTimeMeasured().compareTo(firstTimestamp) < 0
                    || (lastTimestamp != null && obs.getTimeMeasured().compareTo(lastTimestamp) > 0)
            )                
            {
                continue;
            }
            if(separatedParameters.get(obs.getElementMeasurementTypeId()) == null)
            {
                separatedParameters.put(obs.getElementMeasurementTypeId(), new ArrayList<WeatherObservation>());
            }
            
            separatedParameters.get(obs.getElementMeasurementTypeId()).add(obs);
            if(estimatedLastTimestamp == null || estimatedLastTimestamp.compareTo(obs.getTimeMeasured()) < 0)
            {
                estimatedLastTimestamp = obs.getTimeMeasured();
            }
            
        }
        // Correcting last timestamp if necessary
        lastTimestamp = lastTimestamp != null ? lastTimestamp : estimatedLastTimestamp;
            
        // Filling holes
        for(String parameterName:separatedParameters.keySet())
        {
            separatedParameters.put(parameterName,this.checkForAndFixHourlyTimeSeriesHoles(separatedParameters.get(parameterName)));
        }
        // Comparing stuff, trying to fix if not fixed
        // For precipitation, set artificial values to 0
        // For all other values, set to same as copying value
        // Maximum number of missing values at beginning or end: 3
        Integer maxMissing = 3;
        if(!areListsComplete(separatedParameters, firstTimestamp, lastTimestamp))
        {
            Calendar cal = Calendar.getInstance();
            for(String parameterName:separatedParameters.keySet())
            {
                //System.out.println("Checking " + parameterName);
                
                List<WeatherObservation> list = separatedParameters.get(parameterName);
                //System.out.println("First = " + list.get(0).getTimeMeasured());
                //System.out.println("Last = " + list.get(list.size()-1).getTimeMeasured());
                Integer counter = 0;
                while(list.get(0).getTimeMeasured().compareTo(firstTimestamp) != 0 && counter++ < maxMissing)
                {
                    cal.setTime(list.get(0).getTimeMeasured());
                    cal.add(Calendar.HOUR_OF_DAY, -1);
                    Date hourBefore = cal.getTime();
                    WeatherObservation newObs = new WeatherObservation(list.get(0));
                    newObs.setTimeMeasured(hourBefore);
                    if(newObs.getElementMeasurementTypeId().equals(WeatherElements.PRECIPITATION))
                    {
                        newObs.setValue(0.0);
                    }
                    //System.out.println("Prepending this: " + newObs);
                    list.add(0, newObs);
                }

                counter = 0;
                while(list.get(list.size()-1).getTimeMeasured().compareTo(lastTimestamp) != 0 && counter++ < maxMissing)
                {
                    cal.setTime(list.get(list.size()-1).getTimeMeasured());
                    cal.add(Calendar.HOUR_OF_DAY, 1);
                    Date hourAfter = cal.getTime();
                    WeatherObservation newObs = new WeatherObservation(list.get(list.size()-1));
                    newObs.setTimeMeasured(hourAfter);
                    if(newObs.getElementMeasurementTypeId().equals(WeatherElements.PRECIPITATION))
                    {
                        newObs.setValue(0.0);
                    }
                    //System.out.println("Appending this: " + newObs);
                    list.add(newObs);
                }
            }
        }
        
        // Last check, give up if not fixed
        if(areListsComplete(separatedParameters, firstTimestamp, lastTimestamp))
        {
            List<WeatherObservation> retVal = new ArrayList<WeatherObservation>();
            for(String parameterName:separatedParameters.keySet())
            {
                retVal.addAll(separatedParameters.get(parameterName));
            }
            return retVal;
        }
        else
        {
            String errorMessage = "Cant' repair the weather data. Giving up now. "
                    + "Either this algorithm needs improvement, or the dataset is too incomplete. "
                    + "List sizes for the different parameters are: ";
            for(String parameterName:separatedParameters.keySet())
            {
                errorMessage += parameterName + "[Starts at " + separatedParameters.get(parameterName).get(0).getTimeMeasured() + "]: " + separatedParameters.get(parameterName).size() + "\n ";
            }
            throw new WeatherObservationListException(errorMessage);
        }
    }
    
    public List<WeatherObservation> fillHourlyHolesBruteForce(List<WeatherObservation> shitIn, Integer typeOfParameter, Date startDate, Date endDate)
    {
        Calendar cal = Calendar.getInstance();
        Collections.sort(shitIn);
        List<WeatherObservation> shitOut = new ArrayList<>();
        for(Integer i=0;i< shitIn.size();i++){
            WeatherObservation currentObs = shitIn.get(i);
            if(! currentObs.getTimeMeasured().after(startDate))
            {
                continue;
            }
            shitOut.add(currentObs);
            cal.setTime(currentObs.getTimeMeasured());
            cal.add(Calendar.HOUR_OF_DAY, 1);
            Date currentTime = cal.getTime();
            Date nextObsTimeMeasured = i+1 < shitIn.size() ? shitIn.get(i+1).getTimeMeasured() : endDate;
            while(currentTime.before(nextObsTimeMeasured))
            {
                
                WeatherObservation fakeObs = new WeatherObservation(currentObs);
                fakeObs.setTimeMeasured(currentTime);
                // Make sure we don't add any extra e.g. for rain
                if(typeOfParameter == WeatherUtil.AGGREGATION_TYPE_SUM)
                {
                    fakeObs.setValue(0.0);
                }
                shitOut.add(fakeObs);
                cal.setTime(currentTime);
                cal.add(Calendar.HOUR_OF_DAY, 1);
                currentTime = cal.getTime();
            }
            
        }
        
        return shitOut;
    }
    
    /**
     * Checks for duplicates, attempts to tidy up. TODO: Criteria for selection??
     * @param observations List with possible duplicates (same parameter, logInterval and timestamp)
     * @param maximumDuplicateRatio Between 0 and 1. 0 = No duplicates allowed. 1 = 100% duplicates. (Which is impossible) Default is 0.05 (5%)
     * @return List with unique observations, unsorted
     */
    public List<WeatherObservation> removeDuplicateWeatherObservations(List<WeatherObservation> observations, Double maximumDuplicateRatio) throws WeatherObservationListException
    {
        
        if(maximumDuplicateRatio == null)
        {
            maximumDuplicateRatio = 0.05;
        }
        HashMap<Long,WeatherObservation> uniqueMap = new HashMap<Long, WeatherObservation>();
        for(WeatherObservation observation:observations)
        {
            uniqueMap.put(observation.getValiditySignature(), observation);
        }
        List<WeatherObservation> retVal = new ArrayList<WeatherObservation>(uniqueMap.values());
        Double numberOfDuplicates = new Double(observations.size() - retVal.size());
        //System.out.println(numberOfDuplicates/observations.size());
        if(numberOfDuplicates/observations.size() > maximumDuplicateRatio)
        {
            throw new WeatherObservationListException("Too many duplicates for " + observations.get(0).getElementMeasurementTypeId() + ": " + numberOfDuplicates + "(" + (numberOfDuplicates/observations.size()) + "%)");
        }
        return retVal;
    }
    
    /**
     * Similar to isHomogenousTimeseries, but adds extra restriction with regards to period
     * @param separatedParameters
     * @param firstTimestamp
     * @param lastTimestamp
     * @return 
     */
    private Boolean areListsComplete(Map<String, List<WeatherObservation>> separatedParameters, Date firstTimestamp, Date lastTimestamp)
    {
        Integer size = null;
        for(String parameterName:separatedParameters.keySet())
        {
            List<WeatherObservation> currentList = separatedParameters.get(parameterName);
            Collections.sort(currentList);
            // Check for size
            if(size == null)
            {
                size = currentList.size();
            }
            if(size != currentList.size())
            {
                return Boolean.FALSE;
            }
            // Check for period
            if(!currentList.isEmpty())
            {
                if(currentList.get(0).getTimeMeasured().compareTo(firstTimestamp) != 0)
                {
                    return Boolean.FALSE;
                }
                if(lastTimestamp == null)
                {
                    lastTimestamp = currentList.get(currentList.size()-1).getTimeMeasured();
                }
                if(currentList.get(currentList.size()-1).getTimeMeasured().compareTo(lastTimestamp) != 0)
                {
                    return Boolean.FALSE;
                }
            }
        }
        return Boolean.TRUE;
    }
    
    public String dumpWeatherObservationList(List<WeatherObservation> list)
    {
        Collections.sort(list);
        StringBuilder retVal = new StringBuilder();
        for(WeatherObservation obs:list)
        {
            retVal.append(obs.getElementMeasurementTypeId()).append("[").append(obs.getTimeMeasured()).append("] : ").append(obs.getValue()).append("\n");
        }
        return retVal.toString();
    }
    
    /**
     * Searches a time series of one parameter for holes
     * @param observations
     * @param logInterval @see WeatherObservation
     * @return 
     */
    public Date findFirstHoleInObservationSeries(List<WeatherObservation> observations, Integer logInterval, TimeZone timeZone)
    {
        Collections.sort(observations);
        Date currentTime = observations.get(0).getTimeMeasured();
        Calendar cal = Calendar.getInstance(timeZone);
        for(WeatherObservation obs:observations)
        {
            if(obs.getTimeMeasured().compareTo(currentTime) != 0)
            {
                return currentTime;
            }
            cal.setTime(currentTime);
            int interval = logInterval.equals(WeatherObservation.LOG_INTERVAL_ID_1H) ? Calendar.HOUR_OF_DAY : Calendar.DATE;
            cal.add(interval, 1);
            currentTime = cal.getTime();
        }
        return null;
    }
    
    /**
     * Basic weather data time series health check. Same length? Starts and
     * ends at same time?
     * @param series
     * @return true if passed all tests
     * @throws WeatherObservationListException if at least one test fails
     */
    public boolean isHomogenousTimeSeries(List<WeatherObservation>... series) throws WeatherObservationListException
    {
        // Must be of same length
        for(List<WeatherObservation> list:series)
        {
            // If a list has no elements, it's not a time series
            if(list == null || list.isEmpty())
            {
                throw new WeatherObservationListException("At least one list is empty or NULL");
            }
            Collections.sort(list);
            // Must be of same length
            if(series[0].size() != list.size())
            {
                StringBuilder msg = new StringBuilder("Unequal length of lists.");
                for (List<WeatherObservation> serie : series) {
                    msg     .append("List with ")
                            .append(serie.get(0).getElementMeasurementTypeId())
                            .append(" length=")
                            .append(serie.size())
                            .append(".\n");
                }
                throw new WeatherObservationListException(msg.toString());
            }
            // Must be starting at same time
            if(series[0].get(0).getTimeMeasured().compareTo(list.get(0).getTimeMeasured()) != 0)
            {
                StringBuilder msg = new StringBuilder("Weather parameter lists do not start at same time");
                for (List<WeatherObservation> serie : series) {
                    msg     .append("List with ")
                            .append(serie.get(0).getElementMeasurementTypeId())
                            .append(" starts at ")
                            .append(serie.get(0).getTimeMeasured())
                            .append(".\n");
                }
                throw new WeatherObservationListException(msg.toString());
            }
            // Must be ending at same time
            int lastIndex = series[0].size() - 1;
            if(series[0].get(lastIndex).getTimeMeasured().compareTo(list.get(lastIndex).getTimeMeasured()) != 0)
            {
                StringBuilder msg = new StringBuilder("Weather parameter lists do not end at same time");
                for (List<WeatherObservation> serie : series) {
                    msg     .append("List with ")
                            .append(serie.get(lastIndex).getElementMeasurementTypeId())
                            .append(" ends at ")
                            .append(serie.get(lastIndex).getTimeMeasured())
                            .append(".\n");
                }
                throw new WeatherObservationListException(msg.toString());
            }
        }
        return true;
    }

    /**
     * Finds the last timestamp for which all parameters are present, and 
     * removes all observations after that
     * @param observations
     * @return 
     */
    public List<WeatherObservation> truncateToLastCommonObservation(List<WeatherObservation> observations) {
        // Find the latest date for each parameter
        Map<String, Date> latestObservations = new HashMap<String, Date>();
        for(WeatherObservation obs:observations)
        {
            Date latestDateForParameter = latestObservations.get(obs.getElementMeasurementTypeId());
            latestDateForParameter = latestDateForParameter == null ? obs.getTimeMeasured() : 
                    obs.getTimeMeasured().compareTo(latestDateForParameter) > 0 ? obs.getTimeMeasured() : latestDateForParameter;
            latestObservations.put(obs.getElementMeasurementTypeId(), latestDateForParameter);
        }
        
        // The earliest date is the latest common date
        Date latestCommonDate = null;
        for(Date date:latestObservations.values())
        {
            latestCommonDate = latestCommonDate == null ? date:
                    date.compareTo(latestCommonDate) < 0 ? date : latestCommonDate;
        }
        // Then we filter out all the observations after the latestCommonDate
        List<WeatherObservation> retVal = new ArrayList<WeatherObservation>();
        for(WeatherObservation obs:observations)
        {
            if(obs.getTimeMeasured().compareTo(latestCommonDate) <= 0)
            {
                retVal.add(obs);
            }
        }
        
        return retVal;
    }
    
    /**
     * Finds the first and last timestamp for which all parameters are present, and 
     * removes all observations before and after that, respectively
     * @param observations
     * @return 
     */
    public List<WeatherObservation> truncateToFirstAndLastCommonObservation(List<WeatherObservation> observations) {
        // Find the earliest and latest date for each parameter
        Map<String, Date> latestObservations = new HashMap<String, Date>();
        Map<String, Date> earliestObservations = new HashMap<String, Date>();
        for(WeatherObservation obs:observations)
        {
            Date latestDateForParameter = latestObservations.get(obs.getElementMeasurementTypeId());
            latestDateForParameter = latestDateForParameter == null ? obs.getTimeMeasured() : 
                    obs.getTimeMeasured().compareTo(latestDateForParameter) > 0 ? obs.getTimeMeasured() : latestDateForParameter;
            latestObservations.put(obs.getElementMeasurementTypeId(), latestDateForParameter);
            Date earliestDateForParameter = earliestObservations.get(obs.getElementMeasurementTypeId());
            earliestDateForParameter = earliestDateForParameter == null ? obs.getTimeMeasured() :
                    obs.getTimeMeasured().compareTo(earliestDateForParameter) < 0 ? obs.getTimeMeasured() : earliestDateForParameter;
            earliestObservations.put(obs.getElementMeasurementTypeId(), earliestDateForParameter);
        }
        
        // Finding first common date
        // The latest date is the first common date
        Date earliestCommonDate = null;
        for(Date date:earliestObservations.values())
        {
            earliestCommonDate = earliestCommonDate == null ? date :
                    date.compareTo(earliestCommonDate) > 0 ? date : earliestCommonDate;
        }
        
        // Finding latest common date
        // The earliest date is the latest common date
        Date latestCommonDate = null;
        for(Date date:latestObservations.values())
        {
            latestCommonDate = latestCommonDate == null ? date:
                    date.compareTo(latestCommonDate) < 0 ? date : latestCommonDate;
        }
        // Then we filter out all the observations before the earliestCommonDate and after the latestCommonDate
        List<WeatherObservation> retVal = new ArrayList<WeatherObservation>();
        for(WeatherObservation obs:observations)
        {
            if(obs.getTimeMeasured().compareTo(earliestCommonDate) >= 0 || obs.getTimeMeasured().compareTo(latestCommonDate) <= 0)
            {
                retVal.add(obs);
            }
        }
        
        return retVal;
    }
    
    /**
     * Extracts only the parameters that you want
     * @param observations
     * @param parameters
     * @return 
     */
    public List<WeatherObservation> filterWeatherObservationsByParameter(List<WeatherObservation> observations, Set<String> parameters){
        List<WeatherObservation> retVal = new ArrayList<WeatherObservation>();
        for(WeatherObservation obs:observations)
        {
            if(parameters.contains(obs.getElementMeasurementTypeId()))
            {
                retVal.add(obs);
            }
        }
        return retVal;
    }
    
    /**
     * 
     * @param observations
     * @param timeZone
     * @param logintervalId
     * @param typeOfAggregation
     * @return
     * @throws WeatherObservationListException
     * @throws InvalidAggregationTypeException 
     */
    public List<WeatherObservation> getAggregateHourlyValues(List<WeatherObservation> observations, TimeZone timeZone, Integer logintervalId, Integer typeOfAggregation) throws WeatherObservationListException, InvalidAggregationTypeException{
        if(observations == null || observations.isEmpty())
        {
            return null;
        }
        // First we organize the less-than-hourly values into one bucket per hour
        Map<Date,Map> hourBucket = new HashMap<Date,Map>();
        String expectedParameter = observations.get(0).getElementMeasurementTypeId();
        Date lastDate = null;
        for(WeatherObservation observation:observations)
        {
            if(!observation.getElementMeasurementTypeId().equals(expectedParameter))
            {
                throw new WeatherObservationListException("Found multiple parameters: " + observation.getElementMeasurementTypeId() + " and " + expectedParameter);
            }
            Date theDate = normalizeToExactHour(observation.getTimeMeasured(), timeZone);
            lastDate = lastDate == null ? theDate : (lastDate.compareTo(theDate) < 0 ? theDate : lastDate);
            Map<Date, Double> hourValuesForDate = hourBucket.get(theDate);
            if(hourValuesForDate == null)
            {
                hourValuesForDate = new HashMap<Date,Double>();
                hourBucket.put(theDate, hourValuesForDate);
            }
            
            // Check for double entries
            // TODO: Handle DST change with double entries at 03:00
            Double possibleDuplicate = hourValuesForDate.get(observation.getTimeMeasured());
            if(possibleDuplicate != null)
            {
                throw new WeatherObservationListException(
                        "Found duplicate weatherObservations for parameter " +
                        observation.getElementMeasurementTypeId() + " at time " +
                        observation.getTimeMeasured()
                );
            }
            hourValuesForDate.put(observation.getTimeMeasured(), observation.getValue());
        }
        
        // Then we iterate the buckets, do the aggregation and create return values
        List<WeatherObservation> aggregatedObservations = new ArrayList<WeatherObservation>();
        WeatherObservation templateObservation = observations.get(0);
        Double aggregateValue;
        for(Date anHour:hourBucket.keySet())
        {
            //System.out.println("date=" + aDay);
            Map valuesForAnHour = hourBucket.get(anHour);
            
            switch(typeOfAggregation){
                case WeatherUtil.AGGREGATION_TYPE_AVERAGE:
                    aggregateValue = getAverage(valuesForAnHour.values()); break;
                case WeatherUtil.AGGREGATION_TYPE_SUM:
                    aggregateValue = getSum(valuesForAnHour.values()); break;
                case WeatherUtil.AGGREGATION_TYPE_MINIMUM:
                    aggregateValue = getMinimum(valuesForAnHour.values()); break;
                case WeatherUtil.AGGREGATION_TYPE_MAXIMUM:
                    aggregateValue = getMaximum(valuesForAnHour.values()); break;
                default:
                    throw new InvalidAggregationTypeException(
                            "No aggregation method with id= " + typeOfAggregation  + " exists."
                            );
            }
            WeatherObservation aggregatedObservation = new WeatherObservation();
            aggregatedObservation.setElementMeasurementTypeId(templateObservation.getElementMeasurementTypeId());
            aggregatedObservation.setLogIntervalId(WeatherObservation.LOG_INTERVAL_ID_1H);
            aggregatedObservation.setTimeMeasured(anHour);
            aggregatedObservation.setValue(aggregateValue);
            aggregatedObservations.add(aggregatedObservation);
        }
        return aggregatedObservations;
    }
    
    /**
     * Adds the correct amount of time for the next time, given log interval
     * Attempts to correct if starting point is a bit off
     * @param startingPoint The timestamp to start with
     * @param logIntervalId The log frequency (see VIPSCommon's WeatherObservation entity for details)
     * @param timeZone The time zone
     * @return 
     */
    public Date getNextLogTimestamp(Date startingPoint, Integer logIntervalId, TimeZone timeZone)
    {
        Calendar cal = Calendar.getInstance(timeZone);
        cal.setTime(startingPoint);
        
        if(logIntervalId.equals(WeatherObservation.LOG_INTERVAL_ID_15M))
        {
            cal.add(Calendar.MINUTE, 15);
            cal.set(Calendar.MINUTE, cal.get(Calendar.MINUTE) - (cal.get(Calendar.MINUTE) % 15));
        }
        else if(logIntervalId.equals(WeatherObservation.LOG_INTERVAL_ID_30M))
        {
            cal.add(Calendar.MINUTE, 30);
            cal.set(Calendar.MINUTE, cal.get(Calendar.MINUTE) - (cal.get(Calendar.MINUTE) % 30));
        }
        else if(logIntervalId.equals(WeatherObservation.LOG_INTERVAL_ID_1H))
        {
            cal.add(Calendar.HOUR_OF_DAY, 1);
            cal.set(Calendar.MINUTE, 0);
        }
        else if(logIntervalId.equals(WeatherObservation.LOG_INTERVAL_ID_3H))
        {
            cal.add(Calendar.HOUR_OF_DAY, 3);
            cal.set(Calendar.MINUTE, 0);
        }
        else if(logIntervalId.equals(WeatherObservation.LOG_INTERVAL_ID_6H))
        {
            cal.add(Calendar.HOUR_OF_DAY, 6);
            cal.set(Calendar.MINUTE, 0);
        }
        else if(logIntervalId.equals(WeatherObservation.LOG_INTERVAL_ID_1D))
        {
            cal.add(Calendar.DATE,1);
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
        }
        
        return cal.getTime();
    }
    
    public List<WeatherObservation> getIncrementalValuesFromAccumulated(List<WeatherObservation> accumulatedValues, TimeZone timeZone, Integer incrementalLogIntervalId)
    {
        List<WeatherObservation> retVal = new ArrayList<WeatherObservation>();
        // We always start on the whole hour
        Calendar cal = Calendar.getInstance(timeZone);
        Collections.sort(accumulatedValues);
        Date nextIncrementalTimestamp = null;
        Double lastAccumulatedValue = null;
        for(WeatherObservation obs : accumulatedValues)
        {
            
            //System.out.println("nextIncrementalTimestamp=" + nextIncrementalTimestamp + ", lastAccumulatedValue=" + lastAccumulatedValue);
            if(lastAccumulatedValue != null)
            {
                if(nextIncrementalTimestamp.compareTo(obs.getTimeMeasured()) == 0)
                {
                    // Ensure that reset of [XX]ACC counter doesn't give negative values
                    Double incrementalValue = Math.max(obs.getValue() - lastAccumulatedValue, 0.0);
                    WeatherObservation incrementObs = new WeatherObservation();
                    incrementObs.setElementMeasurementTypeId(obs.getElementMeasurementTypeId().replace("ACC", ""));
                    incrementObs.setLogIntervalId(incrementalLogIntervalId);
                    incrementObs.setTimeMeasured(obs.getTimeMeasured());
                    incrementObs.setValue(incrementalValue);
                    retVal.add(incrementObs);
                    lastAccumulatedValue = obs.getValue();
                    nextIncrementalTimestamp = this.getNextLogTimestamp(obs.getTimeMeasured(), incrementalLogIntervalId, timeZone);
                }
                // CASE: Hole in the raw data
                else if(nextIncrementalTimestamp.compareTo(obs.getTimeMeasured()) < 0)
                {
                    nextIncrementalTimestamp = this.getNextLogTimestamp(obs.getTimeMeasured(), incrementalLogIntervalId, timeZone);
                }
            }
            // We always start on the whole hour, and in case of daily values,
            // on the hour = 00
            else
            {
                cal.setTime(obs.getTimeMeasured());
                if(
                        cal.get(Calendar.MINUTE) == 0
                        &&
                            (!incrementalLogIntervalId.equals(WeatherObservation.LOG_INTERVAL_ID_1D)
                            || cal.get(Calendar.HOUR_OF_DAY) == 0
                            )
                        )

                {
                    lastAccumulatedValue = obs.getValue();
                    nextIncrementalTimestamp = this.getNextLogTimestamp(obs.getTimeMeasured(), incrementalLogIntervalId, timeZone);
                }
            }
        }
        return retVal;
    }
    
    /**
     * Mining in list, picking the numberOfObservations last observations of the requested weatherElement
     * @param allObservations
     * @param weatherElement
     * @param numberOfObservations
     * @return 
     */
    public List<WeatherObservation> getLastObservations(List<WeatherObservation> allObservations, String weatherElement, int numberOfObservations)
    {
        PriorityQueue<WeatherObservation> lastObs = new PriorityQueue<>();
        for(WeatherObservation obs:allObservations)
        {
            if(obs.getElementMeasurementTypeId().equals(weatherElement))
            {
                lastObs.add(obs);
                if(lastObs.size() > numberOfObservations)
                {
                    lastObs.poll();
                }
            }
        }
        return new ArrayList(lastObs);
    }
    
    public List<WeatherObservation> getFirstObservations(List<WeatherObservation> allObservations, String weatherElement, int numberOfObservations)
    {
        PriorityQueue<WeatherObservation> firstObs =  new PriorityQueue<>(numberOfObservations, Collections.reverseOrder());
        for(WeatherObservation obs:allObservations)
        {
            if(obs.getElementMeasurementTypeId().equals(weatherElement))
            {
                firstObs.add(obs);
                if(firstObs.size() > numberOfObservations)
                {
                    firstObs.poll();
                }
            }
        }
        List<WeatherObservation> retVal = new ArrayList(firstObs);
        Collections.sort(retVal);
        return retVal;
    }
}