gui15minTimeseries_Model.java [src/java/m/cfa/timeseries15min] Revision:   Date:
package m.cfa.timeseries15min;

import WaterData.WaterData;
import WaterData.WaterDataInterface;
import m.cfa.DateComparator;
import m.cfa.DoubleArray;
import m.cfa.DoubleMath;
import m.cfa.Graphing;
import java.awt.Color;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.general.SeriesException;
import org.jfree.data.time.Day;
import org.jfree.data.time.Minute;
import org.jfree.data.time.TimeSeries;

/**
* Last Updated: 9-April-2019
* @author Tyler Wible
* @since 23-June-2014
*/
public class gui15minTimeseries_Model {
    String directory = "E:/Projects/TylerWible_repos/NetBeans/data/CFA/Timeseries";
    String database = "USGS";//"CDWR";
    String stationId = "06752260";//"CLAGRECO";
    String stationName = "CACHE LA POUDRE RIVER AT FORT COLLINS, CO";//"Cache La Poudre Near Greeley";
    String startDate = "";//yyyy-MM-dd
    String endDate = "";//yyyy-MM-dd
    String userData = "";//"Date\tFlow\n1999-04-29 00:00\t8.3\n1999-05-09 00:00\t60.2\n1999-05-29 00:00\t20.1";//
    boolean mergeDatasets = false;//true;//
    String mergeMethod = "user";//"public";//"max";//"average";//"min";//
    
    
    //Outputs
    String len = "-1";
    String start = "?";
    String end = "?";
    String units = "?";
    String dataSource = "?";
    double max = -1;
    double min = -1;
    double range = -1;
    double mean = -1;
    double standardDeviation = -1;
    
    //Gets
    public File getOutputSummary() {
        return new File(directory, "timeseries15min_summary.csv");
    }
    public String getGraph() {
        return "timeseries15min_graph.jpg";
    }
    public File getTimeseriesOutput(){
        //This output file is for use with JSHighCharts
        return new File(directory, "timeseries15min_graph.out");
    }
    public String getLen(){ return len; }
    public String getStart(){ return start; }
    public String getEnd(){ return end; }
    public String getUnits(){ return units; }
    public String getDataSource(){ return dataSource; }
    public String getMax(){ return String.valueOf(max); }
    public String getMin(){ return String.valueOf(min); }
    public String getRange(){ return String.valueOf(range); }
    public String getMean(){ return String.valueOf(mean); }
    public String getStandardDeviation(){ return String.valueOf(standardDeviation); }
    
    //Sets
    public void setDirectory(String directory_str){ directory = directory_str; }
    public void setDatabase(String database_str){ database = database_str; }
    public void setStationId(String stationId_str){ stationId = stationId_str; }
    public void setStationName(String stationName_str){ stationName = stationName_str; }
    public void setStartDate(String startDate_str){ startDate = startDate_str; }
    public void setEndDate(String endDate_str){ endDate = endDate_str; }
    public void setUserData(String userData_str){ userData = userData_str; }
    public void setMergeDatasets(boolean mergeDatasets_TF){ mergeDatasets = mergeDatasets_TF; }
    public void setMergeMethod(String mergeMethod_str){ mergeMethod = mergeMethod_str; }
    
    /**
     * Main statistics function calls other functions to calculate each statistic value then stores the results as global variables
     * @param sortedData_combined  data on which statistical values are desired
     */
    private void CalculateDailyStatistics(String[][] flowData) throws IOException, ParseException {
        //Calculate flow statistics for the entire analysis period
        ArrayList<Double> allData = new ArrayList<>();
        for(int i=0; i<flowData.length; i++){
            allData.add(Double.parseDouble(flowData[i][1]));
        }
        
        String[][] resultSummary = new String[6][1];
        resultSummary[0][0] = "Method";
        resultSummary[1][0] = "Maximum";
        resultSummary[2][0] = "Minimum";
        resultSummary[3][0] = "Range";
        resultSummary[4][0] = "Mean";
        resultSummary[5][0] = "Standard Deviation";
        
        resultSummary = CalculateStatistics(allData, resultSummary, "All Data " + start + " to " + end);
        max = Double.parseDouble(resultSummary[1][1]);
        min = Double.parseDouble(resultSummary[2][1]);
        range = Double.parseDouble(resultSummary[3][1]);
        mean = Double.parseDouble(resultSummary[4][1]);
        standardDeviation = Double.parseDouble(resultSummary[5][1]);
        
        //Calculate Flow statistics for each day in time period
        boolean moreDays = flowData.length > 0;
        String currentDay = flowData[0][0].substring(0,10);
        String finalDay = flowData[flowData.length - 1][0].substring(0,10);
        while(moreDays){
            //Get current year's data and calculate it's statistics
            ArrayList<Double> partialData = DoubleArray.getDaysData(flowData, currentDay);
            resultSummary = CalculateStatistics(partialData, resultSummary, currentDay);
            
            String nextDay = DoubleArray.getDay(currentDay, 1);
            if(finalDay.compareToIgnoreCase(String.valueOf(nextDay)) >= 0){
                currentDay = String.valueOf(nextDay);
            }else{
                moreDays = false;
            }
        }
        
        writeSummary(resultSummary);
    }
    private String[][] CalculateStatistics(ArrayList<Double> dataList, String[][] resultSummary, String header){
        double maxVal = DoubleMath.max(dataList);
        double minVal = DoubleMath.min(dataList);
        double rangeVal = maxVal - minVal;
        double aveVal = DoubleMath.meanArithmetic(dataList);
        double stDev = DoubleMath.StandardDeviationSample(dataList);
        
        //Append current results to summary
        String[] resultArray = {header, 
            String.valueOf(DoubleMath.round(maxVal,3)), 
            String.valueOf(DoubleMath.round(minVal,3)),
            String.valueOf(DoubleMath.round(rangeVal,3)),
            String.valueOf(DoubleMath.round(aveVal,3)),
            String.valueOf(DoubleMath.round(stDev,3))};
        resultSummary = DoubleArray.appendcolumn_Matrix(resultSummary, resultArray);
        
        return resultSummary;
    }
    /**
     * Graph the time series and user data and save the resulting graph to the specified location
     * @param sortedData  the String[][] containing sorted data for the time series 
     * (column 1 = dates (yyyy-mm-dd if timeStep = "daily", yyyy-mm if 
     * timeStep = "monthly", yyyy if timeStep = "yearly") column 2 = value
     * @param sortedData_user  the String[][] containing sorted user data for the 
     * time series (column 1 = dates (yyyy-mm-dd if timeStep = "daily", yyyy-mm 
     * if timeStep = "monthly", yyyy if timeStep = "yearly") column 2 = value
     * @param yAxisTitle  the String label for the y axis of the graph
     */
    private void createTimeseriesGraph(String[][] sortedData,
                                       String[][] sortedData_user) throws ParseException, IOException {
        //Create TimeSeries graph of merged data
        TimeSeries series = new TimeSeries(stationId + ": Data");
        DateFormat desiredDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        Date jfreeChartDateLimit = desiredDateFormat.parse("1900-01-01 00:00");
        String[][] graphData = new String[sortedData.length][2];
        for(int i=0; i < sortedData.length; i++) {
            double value = Double.parseDouble(sortedData[i][1]);
            Date currentDate = desiredDateFormat.parse(sortedData[i][0]);
            Minute date =  new Minute(currentDate);
            try{
                series.add(date, value);
                graphData[i][0] = sortedData[i][0];
                graphData[i][1] = sortedData[i][1];
            }catch(SeriesException e){
                //Some times there are multiple data points for a single yyyy-MM-dd HH:mm so catch the error here and continue
                graphData[i][0] = "1900-01-01 00:00";
                graphData[i][1] = "-1";
            }catch(IllegalArgumentException e){
                //If the date is prior to 1900-01-01, JFreechart will throw and error trying to parse it into the graph.
                //However, keep the data for the dynamic graph which does not have this limit
                graphData[i][0] = sortedData[i][0];
                graphData[i][1] = sortedData[i][1];
            }
        }
        
        //Create Time Series graph of user data
        TimeSeries series2 = new TimeSeries(stationId + ": User Data");
        for(int i=0; i < sortedData_user.length; i++) {
            double value = Double.parseDouble(sortedData_user[i][1]);
            //artificial limit for JFreeCharts' "Day" class
            Date currentDate = desiredDateFormat.parse(sortedData_user[i][0]);
            if(currentDate.compareTo(jfreeChartDateLimit) >= 0){
                series2.add(new Day(currentDate), value);
            }
        }
        
        //Output XY data for use with JHighCharts
        DoubleArray.writeTimeSeries(directory, graphData, "15-min", getTimeseriesOutput().getName(), false);
		
        //Create renderer, and axis for timeseries graph
        XYPlot plotTime = new XYPlot();
        boolean showLegend = false;
        
        //Create user data points
        if(sortedData_user.length != 0){//only show user points if it is not zero
            plotTime = Graphing.addTimeseriesData(plotTime, series, true, Color.blue, false, false, false, true, 0);
            plotTime = Graphing.addTimeseriesData(plotTime, series2, true, Color.darkGray, false, false, false, true, 1);
            showLegend = true;
        }else{
            plotTime = Graphing.addTimeseriesData(plotTime, series, true, Color.blue, false, false, false, true, 0);
        }
      
        //Create Y Axis
        ValueAxis rangeTime = new NumberAxis("Discharge [cfs]");
        plotTime.setRangeAxis(0, rangeTime);
        
        //Create X Axis
        DateAxis domainTime = new DateAxis("Date");
        domainTime.setLowerMargin(0.05);
        domainTime.setUpperMargin(0.05);
        plotTime.setDomainAxis(0, domainTime);

        //Set extra plot preferences
        plotTime = Graphing.setTimeAxisPreferences(plotTime);

        //Create the chart with the plot and a legend
        String graphTitle = "Time Series for " + database + " Station " + stationId + "; " + stationName;
        JFreeChart chart = new JFreeChart(graphTitle, Graphing.titleFont, plotTime, showLegend);
        
        //Set legend Font
        if(showLegend){
            LegendTitle legendTitle = chart.getLegend();
            legendTitle.setItemFont(Graphing.masterFont);
        }
        
        //Save resulting graph for use later
        try{
            String path = directory + File.separator + getGraph();
            ChartUtilities.saveChartAsJPEG(new File(path), chart, 1280, 800);
            System.out.println("JFreeChart created properly at: " + path);

        }catch(IOException e){
            System.err.println("A problem occurred while trying to creating the chart.");
        }
    }
    /**
     * Writes out the error message, if any, for finding the file and then exits the program
     * @param error  string array to be written as each line of an error message
     * @throws IOException
     */
    public void writeError(ArrayList<String> error) throws IOException{
        //Output data to text file
        String errorContents = error.get(0);
        for(int i=1; i<error.size(); i++){
            errorContents = errorContents + "\n" + error.get(i);
        }
        throw new IOException("Error encountered. Please see the following message for details: \n" + errorContents);
    }
    /**
     * Writes out the dynamically created stats summary of the 15min data
     * @param resultsSummary  string array to be written as each line and column of the csv file
     * @throws IOException
     */
    private void writeSummary(String[][] resultsSummary) throws IOException{
        //Open a file writer for the summary of the flow statistcs
        String path = directory + File.separator + getOutputSummary().getName();
        FileWriter newFile =  new FileWriter(path, false);
        PrintWriter writer = new PrintWriter(newFile);
        
        for(int i=0; i<resultsSummary.length; i++){
            String currentLine = resultsSummary[i][0];
            for(int j=1; j<resultsSummary[i].length; j++){
                currentLine = currentLine + "," + resultsSummary[i][j];
            }
            writer.printf("%s" + "\r\n", 	currentLine);
        }
        
        //Close file writer
        newFile.close();
        writer.close();
        System.out.println("Text File located at:\t" + path);
    }
    public void run() throws IOException, InterruptedException, ParseException, Exception{
        //If no date input, make it the maximum of available data
        if(startDate == null || startDate.equalsIgnoreCase("")){
            if(!database.equalsIgnoreCase("UserData")){
                startDate = "2007-10-01";
            }else{
                startDate = "1850-01-01";
            }
        }
        if(endDate == null || endDate.equalsIgnoreCase("")){
            // Pull current date for upper limit of data search
            DateFormat desiredDateFormat = new SimpleDateFormat("yyyy-MM-dd");
            Date currentDate = new Date();
            endDate = desiredDateFormat.format(currentDate);
        }
        
        //Check if any flow data exists
        WaterDataInterface waterLib = WaterData.getNewWaterDataInterface(database, "");
        String[][] sortableData = waterLib.extractInstantaneousFlowData_formatted(directory, stationId, startDate, endDate);
        dataSource = waterLib.getDataSourceCitation();
        
        //If the user wants the datasets (public and user) merged then retrieve the second dataset (user)
        String[][] sortableData_user = new String[0][0];
        if(mergeDatasets){
            WaterDataInterface waterLibUser = WaterData.getNewWaterDataInterface("UserData", userData);
            sortableData = waterLibUser.extractInstantaneousFlowData_formatted(directory, stationId, startDate, endDate);
        }
        
        //Sort the Data by date to remove duplicate date entries
        Arrays.sort(sortableData, new DateComparator());
        Arrays.sort(sortableData_user, new DateComparator());
        
        //Merge the two datasets (if user data is empty nothing will be merged)
        String[][] sortedData_combined = DoubleArray.mergeData(sortableData, sortableData_user, mergeMethod);
        if(sortedData_combined.length == 0){
            ArrayList<String> errorMessage = new ArrayList<>();
            if(sortableData.length == 0){
                errorMessage.add("There is no available 15-minute (instantaneous) flow data in the " + database + " database for station '" + stationId + "' and the specified date range.");
                if(database.equalsIgnoreCase("CDWR")){
                    errorMessage.add("The CDWR database is sensitive to the begin date used, try specifying a later begin date");
                }
            }
            if(sortableData_user.length == 0){
                errorMessage.add("There is no available 15-minute (instantaneous) uploaded data for station '" + stationId + "' and the specified date range");
            }
            writeError(errorMessage);
        }
        
        //Save analysis results
        this.start = sortedData_combined[0][0];
        this.end = sortedData_combined[sortedData_combined.length - 1][0];
        
        //Calculate stats of data
        CalculateDailyStatistics(sortedData_combined);
        
        //Graph the timeseries data
        createTimeseriesGraph(sortedData_combined, sortableData_user);
        this.len = String.valueOf(sortedData_combined.length);
        this.units = "cfs";
    }
    public static void main(String[] args) throws IOException, InterruptedException, Exception {
        gui15minTimeseries_Model Timeseries15min_Model = new gui15minTimeseries_Model();
        
        //Run model
        Timeseries15min_Model.run();
    }
}