guiDC_Model.java [src/java/m/cfa/durationcurve] Revision:   Date:
package m.cfa.durationcurve;

import WaterData.WaterData;
import WaterData.WaterDataException;
import WaterData.WaterDataInterface;
import WaterData.WaterQualityInfo;
import m.cfa.DoubleArray;
import m.cfa.DoubleMath;
import m.cfa.FlowStatistics;
import m.cfa.Graphing;
import java.awt.Color;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
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.Date;
import org.codehaus.jettison.json.JSONException;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.LogarithmicAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

/**
* Last Updated: 28-August-2020
* @author Tyler Wible
* @since 12-June-2011
*/
public class guiDC_Model {
    //Inputs
    String directory = "C:/Projects/TylerWible_repos/NetBeans/data/CFA/DurationCurve";
    String database = "UserData";//"CDWR";//"STORET";//"CDSN";//"UserData";//
    String orgId = "n/a";//"n/a";//"21COL001";//"CITYFTCO_WQX";//"n/a";//
    String stationId = "09505800";//"CLAGRECO";//"000028";//"1EFF";//"n/a";//
    String stationName = "WestClearCreek_";//"South Platte River at Roscoe, Nebr.";//"Cache La Poudre Near Greeley";//"BIG THOMPSON R NEAR MOUTH";//"n/a";//"n/a";//
    String wqTest = "flow";//"00600 Total nitrogen, water, unfiltered, milligrams per liter -- mg/L";//"00625 Ammonia-nitrogen as N -- mg/L";//
    double wqTarget = 10;//in units of current test
    String startDate = "";
    String endDate = "";
    String seasonBegin = "04-01";//"MM-dd"
    String seasonEnd = "04-30";//"MM-dd"
    boolean seasonalOnly = false;//true;//
    String period1Begin = "";
    String period1End = "";
    String period2Begin = "";
    String period2End = "";
    String period3Begin = "";
    String period3End = "";
    double highPercentile = 0.75;
    double lowPercentile = 0.25;
    boolean showMonthlyStatsTF = false;
    boolean calcFlowStatisticsFileTF = false;
    String mQnPeriod = "false";//"7Q10";//
    boolean mergeDatasets = false;//true;//
    String mergeMethod = "user";//"public";//"max";//"average";//"min";//
    String userData = "";//"Date\tFlow\n1999-04-29\t8.3\n1999-05-09\t60.2\n1999-05-29\t20.1$$Date\t00600\n1999-04-29\t8.3\n1999-05-09\t60.2\n1999-05-29\t20.1";//"Date\tFlow\n1999-04-29\t8.3\n1999-05-09\t60.2\n1999-05-29\t20.1";//
//    String userData = "Date\tFlow\n"+
//            "2020-09-23\t6.15204415970915\n"+
//            "";
    
    //Outputs
    String flowLen = "-1";
    double flowMean = -1;
    double flowMedian = -1;
    String flowLen_period1 = "-1";
    String flowLen_period2 = "-1";
    String flowLen_period3 = "-1";
    String wqLen = "-1";
    String wqUnits = "?";
    String start = "?";
    String end = "?";
    String summaryParagraph = "?";
    String summaryTable = "?";
    String dataSource = "?";
    String lowFlowErrorMessage = "";
    double mQnVal = -1;
    
    
    //Gets
    public File getDurationCurve_results(){ return new File(directory, "duration_curve_results.txt"); }
    public File getFlowStatistics_summary(){
        FlowStatistics flowStats = new FlowStatistics();
        return new File(directory, flowStats.getFlowStatistics_summary());
    }
    public String getGraph() { return "duration_curve_graph.jpg"; }
    public File getDCgraphOutput(){ return new File(directory, "duration_curve_graph.out"); }//for use with JSHighCharts
    public String getFlowLen() { return flowLen; }
    public String getMedian(){ return String.valueOf(flowMedian); }
    public String getMean(){ return String.valueOf(flowMean); }
    public String getFlowLen_period1() { return flowLen_period1; }
    public String getFlowLen_period2() { return flowLen_period2; }
    public String getFlowLen_period3() { return flowLen_period3; }
    public String getWQLen() { return wqLen; }
    public String getWQUnits() { return wqUnits; }
    public String getStart() { return start; }
    public String getEnd() { return end; }
    public String getSummaryParagraph(){ return summaryParagraph; }
    public String getSummaryTable(){ return summaryTable; }
    public String getDataSource(){ return dataSource; }
    public boolean getCalcFlowStatisticsFileTF(){ return calcFlowStatisticsFileTF; }
    public String getLowFlowErrorMessage(){ return lowFlowErrorMessage; }
    public double getMQNval(){ return mQnVal; }
    
    //Sets
    public void setDirectory(String directory_str){ directory = directory_str; }
    public void setDatabase(String database_str){ database = database_str; }
    public void setOrganizationID(String orgId_str){ orgId = orgId_str; }
    public void setStationId(String stationId_str){ stationId = stationId_str; }
    public void setStationName(String stationName_str){ stationName = stationName_str; }
    public void setWaterQualityTest(String wqTest_str){ wqTest = wqTest_str; }
    public void setWaterQualityTarget(double wqTarget_dbl){ wqTarget = wqTarget_dbl; }
    public void setStartDate(String startDate_str){ startDate = startDate_str; }
    public void setEndDate(String endDate_str){ endDate = endDate_str; }
    public void setSeasonBegin(String seasonBegin_str){ seasonBegin = seasonBegin_str; }
    public void setSeasonEnd(String seasonEnd_str){ seasonEnd = seasonEnd_str; }
    public void setSeasonalAnalysisOnlyTF(boolean seasonalOnly_TF){ seasonalOnly = seasonalOnly_TF; }
    public void setPeriod1Begin(String period1Begin_str){ period1Begin = period1Begin_str; }
    public void setPeriod1End(String period1End_str){ period1End = period1End_str; }
    public void setPeriod2Begin(String period2Begin_str){ period2Begin = period2Begin_str; }
    public void setPeriod2End(String period2End_str){ period2End = period2End_str; }
    public void setPeriod3Begin(String period3Begin_str){ period3Begin = period3Begin_str; }
    public void setPeriod3End(String period3End_str){ period3End = period3End_str; }
    public void setHighPercentile(double highPercentile_dbl){ highPercentile = highPercentile_dbl; }
    public void setLowPercentile(double lowPercentile_dbl){ lowPercentile = lowPercentile_dbl; }
    public void setShowMonthlyStatsTF(boolean showMonthlyStats_TF){ showMonthlyStatsTF = showMonthlyStats_TF; }
    public void setCalcFlowStatisticsFileTF(boolean calcFlowStatisticsFile_TF){ calcFlowStatisticsFileTF = calcFlowStatisticsFile_TF; }
    public void setMQNperiod(String mQnPeriod_str){ mQnPeriod = mQnPeriod_str; }
    public void setMergeDatasets(boolean mergeDatasets_TF){ mergeDatasets = mergeDatasets_TF; }
    public void setMergeMethod(String mergeMethod_str){ mergeMethod = mergeMethod_str; }
    public void setUserData(String userData_str){ userData = userData_str; }

    /**
     * Main Flow Duration Curve (FDC) function
     * Creates a graph and dynamic summary for the graph, returns an image and text file in 
     * location "directory"/"fileName"graph.jpg and "directory"/"fileName"paragraph.txt
     * @throws IOException
     * @throws InterruptedException
     * @throws ParseException
     * @throws Exception 
     */
    private void createFDC() throws IOException, ParseException, Exception{
        //Check if user wants mQnFlow calculated and displayed
        boolean mQnHide = mQnPeriod.contains("false");	

        //Check if any flow data exists
        WaterDataInterface waterLib = WaterData.getNewWaterDataInterface(database, userData);
        String[][] sortableData = waterLib.extractFlowData_formatted(directory, orgId, 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_user = waterLibUser.extractFlowData_formatted(directory, orgId, stationId, startDate, endDate);
        }
        
        //Sort the Data by date to remove duplicate date entries
        String[][] sortedData = DoubleArray.removeDuplicateDates(sortableData);
        String[][] sortedData_user = DoubleArray.removeDuplicateDates(sortableData_user);
        
        //Merge the two datasets (if user data is empty nothing will be merged)
        String[][] sortedData_combined = DoubleArray.mergeData(sortedData, sortedData_user, mergeMethod);
        if(sortedData_combined.length == 0){
            ArrayList<String> errorMessage = new ArrayList<>();
            if(sortedData.length == 0){
                errorMessage.add("There is no available 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(sortedData_user.length == 0){
                errorMessage.add("There is no available uploaded data for station " + stationId + " and the specified date range");
            }
            writeError(errorMessage);
        }
        
        //Get period data for analyses
        String[][] period1Data = DoubleArray.getPeriodData(sortedData_combined, period1Begin, period1End);
        String[][] period2Data = DoubleArray.getPeriodData(sortedData_combined, period2Begin, period2End);
        String[][] period3Data = DoubleArray.getPeriodData(sortedData_combined, period3Begin, period3End);
        
        //Check if the user only wants to perform a seasonal analysis (aka a seasonal flow duration curve)
        if(seasonalOnly){
            sortedData_combined = DoubleArray.getSeasonalData(sortedData_combined, seasonBegin, seasonEnd);
            period1Data = DoubleArray.getSeasonalData(period1Data, seasonBegin, seasonEnd);
            period2Data = DoubleArray.getSeasonalData(period2Data, seasonBegin, seasonEnd);
            period3Data = DoubleArray.getSeasonalData(period3Data, seasonBegin, seasonEnd);
        }
        
        //While the dataset is still sorted by date, pull out the mQn value and start/end dates for analysis summary
        double mQn = 0;
        int m=0, n=0;
        if(mQnHide == false){
            //Pull out mQn-period variables
            String mString = mQnPeriod.substring(0,mQnPeriod.indexOf("Q"));
            String nString = mQnPeriod.substring(mQnPeriod.indexOf("Q")+1);
            m = Integer.parseInt(mString);
            n = Integer.parseInt(nString);
            calcFlowStatisticsFileTF = true;
        }
        
        //Calculate the flow stats file
        FlowStatistics flowStats = new FlowStatistics();
        if(calcFlowStatisticsFileTF){
            Object[] returnValue = flowStats.calculateAllStatisticsSummaries(directory, stationId, stationName, sortedData_combined, highPercentile, lowPercentile, 
                    m, n, showMonthlyStatsTF, seasonBegin, seasonEnd, period1Begin, period1End, period2Begin, period2End, period3Begin, period3End, false);
            
            //Calculate low flow
            mQn = (Double) returnValue[0];
            String MQNmessage = (String) returnValue[1];
            if(!mQnHide){
                lowFlowErrorMessage = MQNmessage;
                mQnVal =  DoubleMath.round(mQn, 2);
            }
        }
        
        start = sortedData_combined[0][0];
        end = sortedData_combined[sortedData_combined.length - 1][0];
        flowLen = String.valueOf(sortedData_combined.length);
        flowLen_period1 = String.valueOf(period1Data.length);
        flowLen_period2 = String.valueOf(period2Data.length);
        flowLen_period3 = String.valueOf(period3Data.length);
        CalculateStatistics(sortedData_combined);
        
        //Create Custom Flow Duration Curve graph
        graphFDC(sortedData_combined, sortedData_user, period1Data, period2Data, period3Data, mQn, m, n, mQnHide);
        
        //Attach analysis summary to return object
        summaryParagraph = dynamicParagraph("Flow Duration Curve Overview: ");
    }
    /**
     * Main graphing function for FDC analysis, plots the provided xyRanks as x,y points on a graph 
     * with or without low flow analysis (mQn) added depending if it is non-zero or not

     * @param sortedData_combined  String[][] containing 2 columns, first column dates (yyyy-mm-dd) second column y values of flow of all the data
     * @param sortedData_user  String[][] containing 2 columns, first column dates (yyyy-mm-dd) second column y values of flow of the user data only
     * @param period1Data  the String[][] containing sorted data for the time
     * series contained by period1 (column 1 = dates yyyy-mm-dd, column 2 = value)
     * @param period2Data  the String[][] containing sorted data for the time
     * series contained by period2 (column 1 = dates yyyy-mm-dd, column 2 = value)
     * @param period3Data  the String[][] containing sorted data for the time
     * series contained by period3 (column 1 = dates yyyy-mm-dd, column 2 = value)
     * @param mQn  the low flow analysis value, if it is not zero it will be graphed, otherwise it will not be graphed
     * @param m  the "m" value of an mQn flow analysis (like 7Q10)
     * @param n  the "n" value of an mQn flow analysis (like 7Q10)
     * @param mQnHide  if true then mQn wil be not graphed, otherwise it will be shown in the legend 
     * @throws IOException 
     */
    private void graphFDC(String[][] sortedData_combined, 
                          String[][] sortedData_user,
                          String[][] period1Data,
                          String[][] period2Data,
                          String[][] period3Data,
                          double mQn, 
                          double m, 
                          double n, 
                          boolean mQnHide) throws IOException{
        //Perform Weibull Plotting Position Ranking for the Flow Duration Curve Method
        double[][] xyRanks = DoubleArray.weibullPlottingPosition(sortedData_combined);
        double[][] xyRanks_period1 = DoubleArray.weibullPlottingPosition(period1Data);
        double[][] xyRanks_period2 = DoubleArray.weibullPlottingPosition(period2Data);
        double[][] xyRanks_period3 = DoubleArray.weibullPlottingPosition(period3Data);

        //Determine summary table from xyRanks for duration curve
        calculatePercentileSummmary(xyRanks, xyRanks_period1, xyRanks_period2, xyRanks_period3, "Flow (cfs)");
        
        //Graph the complete flow duration curve for the time period
        XYSeries FDC_xy = new XYSeries("FDC");
        XYSeries mQn_xy = new XYSeries(String.valueOf(m) + "Q" + String.valueOf(n) + " low flow");
        XYSeries FDC_user = new XYSeries("User Data");
        XYSeries FDC_xy_period1 = new XYSeries("Period 1 FDC: " + period1Begin + " to " + period1End);
        XYSeries FDC_xy_period2 = new XYSeries("Period 2 FDC: " + period2Begin + " to " + period2End);
        XYSeries FDC_xy_period3 = new XYSeries("Period 3 FDC: " + period3Begin + " to " + period3End);
        String[][] graphData = new String[xyRanks.length][8];
        int ctr = 0, seriesIndex = 0;
        for(int i=0; i<xyRanks.length; i++){
            FDC_xy.add(xyRanks[i][0], xyRanks[i][1]);//x,y
            graphData[i][0] = String.valueOf(xyRanks[i][0]);
            graphData[i][1] = String.valueOf(xyRanks[i][1]);
            if(i != 0){//get low flow intersection point for mQn flow
                if(xyRanks[i][1] < mQn && xyRanks[i-1][1] >= mQn){
                    mQn_xy.add(xyRanks[i][0], mQn);
                }
            }
            //Create an XYSeries only if userdata is not empty
            if(sortedData_user.length > 0 && sortedData_user.length > ctr){
                double userValue = Double.parseDouble(sortedData_user[ctr][1]);
                if(Double.compare(xyRanks[i][1], userValue) == 0){
                    FDC_user.add(xyRanks[i][0], xyRanks[i][1]);
                    ctr++;
                }
            }
            //Create period 1 line
            if(i < xyRanks_period1.length){
                FDC_xy_period1.add(xyRanks_period1[i][0], xyRanks_period1[i][1]);//x,y
                graphData[i][2] = String.valueOf(xyRanks_period1[i][0]);
                graphData[i][3] = String.valueOf(xyRanks_period1[i][1]);
            }else{
                graphData[i][2] = "-1";
                graphData[i][3] = "-1";
            }
            //Create period 2 line
            if(i < xyRanks_period2.length){
                FDC_xy_period2.add(xyRanks_period2[i][0], xyRanks_period2[i][1]);//x,y
                graphData[i][4] = String.valueOf(xyRanks_period2[i][0]);
                graphData[i][5] = String.valueOf(xyRanks_period2[i][1]);
            }else{
                graphData[i][4] = "-1";
                graphData[i][5] = "-1";
            }
            //Create period 3 line
            if(i < xyRanks_period3.length){
                FDC_xy_period3.add(xyRanks_period3[i][0], xyRanks_period3[i][1]);//x,y
                graphData[i][6] = String.valueOf(xyRanks_period3[i][0]);
                graphData[i][7] = String.valueOf(xyRanks_period3[i][1]);
            }else{
                graphData[i][6] = "-1";
                graphData[i][7] = "-1";
            }
        }

        //Graph resulting FDC and flow value data
        XYPlot plot = new XYPlot();
        boolean showLegend = false;
        //Create X Axis
        ValueAxis xAxis = new NumberAxis("Percent of Time Flow is Exceeded [%]");
        xAxis.setRange(0,100);
        plot.setDomainAxis(0, xAxis);
        
        //Set log-scale y axis
        LogarithmicAxis yAxis = new LogarithmicAxis("Discharge [cfs]");
        yAxis.setAllowNegativesFlag(true); 
        plot.setRangeAxis(0, yAxis);

        //Create a graph with the FDC (line)
        XYDataset FDC_data = new XYSeriesCollection(FDC_xy);
        XYItemRenderer renderer1 = new XYLineAndShapeRenderer(true, false);
        renderer1.setSeriesPaint(0, Color.black);
        //Set the FDC line data, renderer, and axis into plot
        plot.setDataset(seriesIndex, FDC_data);
        plot.setRenderer(seriesIndex, renderer1);
        //Map the line to the first Domain and first Range
        plot.mapDatasetToDomainAxis(seriesIndex, 0);
        plot.mapDatasetToRangeAxis(seriesIndex, 0);
        seriesIndex++;
        
        //Graph period 1 data
        if(period1Data.length > 0){
            XYDataset FDC_period = new XYSeriesCollection(FDC_xy_period1);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false);
            renderer.setSeriesPaint(0, Color.red);
            plot.setDataset(seriesIndex, FDC_period);
            plot.setRenderer(seriesIndex, renderer);
            plot.mapDatasetToDomainAxis(seriesIndex, 0);
            plot.mapDatasetToRangeAxis(seriesIndex, 0);
            seriesIndex++;
            showLegend = true;
        }
        //Graph period 2 data
        if(period2Data.length > 0){
            XYDataset FDC_period = new XYSeriesCollection(FDC_xy_period2);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false);
            renderer.setSeriesPaint(0, new Color(255, 135, 0));//gold
            plot.setDataset(seriesIndex, FDC_period);
            plot.setRenderer(seriesIndex, renderer);
            plot.mapDatasetToDomainAxis(seriesIndex, 0);
            plot.mapDatasetToRangeAxis(seriesIndex, 0);
            seriesIndex++;
            showLegend = true;
        }
        //Graph period 3 data
        if(period3Data.length > 0){
            XYDataset FDC_period = new XYSeriesCollection(FDC_xy_period3);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false);
            renderer.setSeriesPaint(0, Color.green);
            plot.setDataset(seriesIndex, FDC_period);
            plot.setRenderer(seriesIndex, renderer);
            plot.mapDatasetToDomainAxis(seriesIndex, 0);
            plot.mapDatasetToRangeAxis(seriesIndex, 0);
            seriesIndex++;
            showLegend = true;
        }

        //Create low mQn point
        if(!mQnHide && Double.compare(mQn, 0) != 0){//only show mQn if it is not zero
            XYDataset mQn_data = new XYSeriesCollection(mQn_xy);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(false, true);
            renderer.setSeriesShape(0, new Ellipse2D.Double(-3.0, 3.0, 6.0, 6.0));
            renderer.setSeriesPaint(0, Color.red);
            plot.setDataset(seriesIndex,mQn_data);
            plot.setRenderer(seriesIndex,renderer);
            plot.mapDatasetToDomainAxis(seriesIndex,0);
            plot.mapDatasetToRangeAxis(seriesIndex,0);
            seriesIndex++;
            showLegend = true;
        }

        //Create user data points
        if(sortedData_user.length != 0){//only show user points if it is not zero
            XYDataset user_data = new XYSeriesCollection(FDC_user);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(false, true);
            renderer.setSeriesPaint(0, Color.black);
            plot.setDataset(seriesIndex,user_data);
            plot.setRenderer(seriesIndex,renderer);
            plot.mapDatasetToDomainAxis(seriesIndex, 0);
            plot.mapDatasetToRangeAxis(seriesIndex, 0);
            seriesIndex++;
            showLegend = true;
        }

        //Graph a FDC for each year in time period
        String currentYear = start.substring(0,4);
        String finalYear = end.substring(0,4);
        boolean moreYears = xyRanks.length > 0;
        while(moreYears){
            //Get current years data and graph it
            String[][] partialData = DoubleArray.getYearsData(sortedData_combined, currentYear);
            double[][] partialRanks = DoubleArray.weibullPlottingPosition(partialData);
            Graphing.addXYSeries(plot, partialRanks, Color.lightGray, seriesIndex);
            seriesIndex++;
            
            //Save results for output for JHighCharts
            String[] partialFDCData_x = new String[xyRanks.length];
            String[] partialFDCData_y = new String[xyRanks.length];
            for(int i=0; i<xyRanks.length; i++){
                partialFDCData_x[i] = "-1";//value
                partialFDCData_y[i] = "-1";//value
                try{
                    partialFDCData_x[i] = String.valueOf(partialRanks[i][0]);//x
                    partialFDCData_y[i] = String.valueOf(partialRanks[i][1]);//y
                }catch(IndexOutOfBoundsException e){
                    //do nothing as it already has a -1 value
                }
            }
            graphData = DoubleArray.appendcolumn_Matrix(graphData, partialFDCData_x);//Add x points
            graphData = DoubleArray.appendcolumn_Matrix(graphData, partialFDCData_y);//Add y points
            
            //Determine the next data year to continue looping over
            int nextYear = Integer.parseInt(currentYear) + 1;
            if(finalYear.compareToIgnoreCase(String.valueOf(nextYear)) >= 0){
                currentYear = String.valueOf(nextYear);
            }else{
                moreYears = false;
            }
        }
        
        //Output XY data for use with JHighCharts
        DoubleArray.writeXYseries(directory, graphData, getDCgraphOutput().getName());
        
        //Set extra graphing preferences
        plot = Graphing.setLogYaxisPreferences(plot);

        //Add Flow Range Labels:
        plot = Graphing.addIntervalLabel(plot, "High Flow", 0, 10);
        plot = Graphing.addIntervalLabel(plot, "Moist Conditions", 10, 40);
        plot = Graphing.addIntervalLabel(plot, "Mid-Range Flows", 40, 60);
        plot = Graphing.addIntervalLabel(plot, "Dry Conditions", 60, 90);
        plot = Graphing.addIntervalLabel(plot, "Low Flow", 90, 100);

        //Graph plot onto JfreeChart
        String graphTitle = "Flow Duration Curve for " + database + " Station " + stationId + "; " + stationName;
        JFreeChart chart = new JFreeChart(graphTitle, Graphing.titleFont, plot, showLegend);

        //Write a results file containing the flow duration curve interval (exceedance values) and their corresponding discharge values
        writeResults(graphData, "Discharge [cfs]", false);

        //Save resulting graph for proof it works
        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.out.println("A problem occurred while trying to creating the chart for station " + stationName + ". Error: USGSFDC0003");
            System.out.println(e.toString());
        }
    }
    /**
     * Main Load Duration Curve (LDC) function
     * Creates a graph and dynamic summary for the graph, returns an image and text file in 
     * location "directory"/"fileName"graph.jpg and "directory"/"fileName"paragraph.txt
     * @throws IOException
     * @throws InterruptedException
     * @throws ParseException 
     * @throws JSONException
     */
    private void createLDC() throws IOException, InterruptedException, ParseException, JSONException, WaterDataException {
        //Check if any flow and water quality data exists
        
        //Check if any flow and water quality data exists
        String flowData_raw = "", wqData_raw = "";
        if(userData.length() > 0){
            String[] userDataList = userData.split("\\$\\$");
            flowData_raw = userDataList[0];
            wqData_raw = userDataList[1];
        }
        WaterDataInterface waterLib = WaterData.getNewWaterDataInterface(database, flowData_raw);
        String[][] sortableData = waterLib.extractFlowData_formatted(directory, orgId, stationId, startDate, endDate);
        WaterDataInterface waterLib2 = WaterData.getNewWaterDataInterface(database, wqData_raw);
        String[][] WQdata = waterLib2.extractWaterQualityData_formatted(directory, orgId, stationId, startDate, endDate, wqTest);
        dataSource = waterLib.getDataSourceCitation();
        
        //Find the user uploaded data file and uses this for a timeseries graph
        String[][] sortableData_user = new String[0][0];
        String[][] WQdata_user = new String[0][0];
        if(mergeDatasets){
            WaterDataInterface waterLibUser = WaterData.getNewWaterDataInterface("UserData", flowData_raw);
            sortableData_user = waterLibUser.extractFlowData_formatted(directory, orgId, stationId, startDate, endDate);
            WaterDataInterface waterLibUser2 = WaterData.getNewWaterDataInterface("UserData", wqData_raw);
            WQdata_user = waterLibUser2.extractWaterQualityData_formatted(directory, orgId, stationId, startDate, endDate, wqTest);
        }
        
        //Sort the Data by date to remove duplicate date entries
        String[][] sortedData = DoubleArray.removeDuplicateDates(sortableData);
        String[][] sortedData_user = DoubleArray.removeDuplicateDates(sortableData_user);
        
        //Merge the two datasets (if user data is empty nothing will be merged)
        String[][] sortedData_combined = DoubleArray.mergeData(sortedData, sortedData_user, mergeMethod);
        String[][] WQdata_combined = DoubleArray.mergeData(WQdata, WQdata_user, mergeMethod);
        
        //Check if any data exists
        ArrayList<String> errorMessage = new ArrayList<>();
        if(sortedData_combined.length == 0){
            if(sortedData.length == 0){
                errorMessage.add("There is no available 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(sortedData_user.length==0){
                errorMessage.add("There is no uploaded flow data for station " + stationId + " and the specified date range.");
            }
        }
        if(WQdata_combined.length == 0){
            if(WQdata.length == 0){
                errorMessage.add("There are no available " + wqTest + " water quality tests in the " + database + " database for station " + stationId + " and the specified date range.");
            }
            if(WQdata_user.length==0){
                errorMessage.add("There are no uploaded " + wqTest + " water quality tests for station " + stationId+  " and the specified date range");
            }
        }
        if(errorMessage.size() > 0){
            writeError(errorMessage);
        }
        
        //Get Units and conversion for current WQ test
        String[] resultArray = WaterQualityInfo.getWqTestDataInfo(wqTest, database);
        String wqCode = resultArray[0];
        String wqLabel = resultArray[1];
        //String units = resultArray[2];
        double ldcConversion = Double.parseDouble(resultArray[3]);
        String ldcEndUnits = resultArray[4];
	
        //Get period data for analyses
        String[][] period1Data = DoubleArray.getPeriodData(sortedData_combined, period1Begin, period1End);
        String[][] period2Data = DoubleArray.getPeriodData(sortedData_combined, period2Begin, period2End);
        String[][] period3Data = DoubleArray.getPeriodData(sortedData_combined, period3Begin, period3End);
        
        //Check if the user only wants to perform a seasonal analysis (aka a seasonal flow duration curve)
        if(seasonalOnly){
            sortedData_combined = DoubleArray.getSeasonalData(sortedData_combined, seasonBegin, seasonEnd);
            WQdata_combined = DoubleArray.getSeasonalData(WQdata_combined, seasonBegin, seasonEnd);
            period1Data = DoubleArray.getSeasonalData(period1Data, seasonBegin, seasonEnd);
            period2Data = DoubleArray.getSeasonalData(period2Data, seasonBegin, seasonEnd);
            period3Data = DoubleArray.getSeasonalData(period3Data, seasonBegin, seasonEnd);
        }
        
        //While the dataset is still sorted by date, pull out the mQn value and start/end dates for analysis summary
        boolean mQnHide = mQnPeriod.contains("false");
        double mQn = 0;
        int m=0, n=0;
        if(mQnHide == false){
            //Pull out mQn-period variables
            String mString = mQnPeriod.substring(0,mQnPeriod.indexOf("Q"));
            String nString = mQnPeriod.substring(mQnPeriod.indexOf("Q")+1);
            m = Integer.parseInt(mString);
            n = Integer.parseInt(nString);
            calcFlowStatisticsFileTF = true;
        }
        
        //Calculate the flow stats file
        FlowStatistics flowStats = new FlowStatistics();
        if(calcFlowStatisticsFileTF){
            Object[] returnValue = flowStats.calculateAllStatisticsSummaries(directory, stationId, stationName, sortedData_combined, highPercentile, lowPercentile, m, n, 
                    showMonthlyStatsTF, seasonBegin, seasonEnd, period1Begin, period1End, period2Begin, period2End, period3Begin, period3End, false);
            
            //Calculate low flow
            mQn = (Double) returnValue[0];
            String MQNmessage = (String) returnValue[1];
            //Also multiply the mQn flow value(cfs) by the conversion and water quality target
            mQn = mQn*ldcConversion*wqTarget;
            if(!mQnHide){
                if(!MQNmessage.equalsIgnoreCase("")){//if the MQNmessage is not blank add it to the dynamic summary
                    lowFlowErrorMessage = MQNmessage;
                }else{
                    mQnVal =  DoubleMath.round(mQn, 2);
                }
            }
        }
        
        start = sortedData_combined[0][0];
        end = sortedData_combined[sortedData_combined.length - 1][0];
        flowLen = String.valueOf(sortedData_combined.length);
        flowLen_period1 = String.valueOf(period1Data.length);
        flowLen_period2 = String.valueOf(period2Data.length);
        flowLen_period3 = String.valueOf(period3Data.length);
        CalculateStatistics(sortedData_combined);
        
        String[][] seasonalWQ_combined = DoubleArray.getSeasonalData(WQdata_combined, seasonBegin, seasonEnd);
        String[][] seasonalWQ_user = DoubleArray.getSeasonalData(WQdata_user, seasonBegin, seasonEnd);

        //Graph resulting LDC data
        Object[] returnArray = graphLDC(wqCode, wqLabel, ldcEndUnits, ldcConversion, sortedData_combined, sortedData_user, period1Data, period2Data, period3Data, WQdata_combined, WQdata_user, seasonalWQ_combined, seasonalWQ_user,  mQn, m, n);
        String paragraphTitle = (String) returnArray[0];
        double totalCount = (Double) returnArray[1];

        //Attach analysis summary to begin of dynamic paragraph
        wqLen = String.valueOf(totalCount);
        wqUnits = ldcEndUnits;
        summaryParagraph = dynamicParagraph(paragraphTitle);
    }
    /**
     * Sub-graphing function to create combined-range graph
     * @param WQlabel  the title of the water quality test being graphed
     * @param endUnits the final load units of the water quality test * cfs * conversion = load/day
     * @param conversion
     * @param sortedData_combined  the load duration curve XY line data of combined data
     * @param sortedData_user  the load duration curve XY line data of user data only
     * @param period1Data  the String[][] containing sorted data for the time
     * series contained by period1 (column 1 = dates yyyy-mm-dd, column 2 = value)
     * @param period2Data  the String[][] containing sorted data for the time
     * series contained by period2 (column 1 = dates yyyy-mm-dd, column 2 = value)
     * @param period3Data  the String[][] containing sorted data for the time
     * series contained by period3 (column 1 = dates yyyy-mm-dd, column 2 = value)
     * @param WQdata_combined  the observed water quality points XY scatter of combined data
     * @param WQdata_user  the user data water quality points XY scatter of user data only
     * @param seasonalWQ_combined the observed water quality points within the "season" XY scatter of combined data
     * @param seasonalWQ_user the user data water quality points within the "season" XY scatter of user data only
     * @param mQn  the value of the mQn flow (if it is zero then dont show this line in the graph)
     * @param m  the value of m from mQn flow calculations
     * @param n the value of n from mqn flow calculations
     * @return the name/title of the dynamic paragraph to be created to accompany the graph created during this function
     * @throws IOException 
     */
    private Object[] graphLDC(String wqCode,
                              String WQlabel,
                              String endUnits,
                              double conversion,
                              String[][] sortedData_combined,
                              String[][] sortedData_user,
                              String[][] period1Data,
                              String[][] period2Data,
                              String[][] period3Data,
                              String[][] WQdata_combined,
                              String[][] WQdata_user,
                              String[][] seasonalWQ_combined,
                              String[][] seasonalWQ_user,
                              double mQn,
                              double m,
                              double n) throws IOException {
        //Perform Weibull Plotting Position Ranking for the Load Duration Curve Method 
        //Note: this data is already converted to loads, see "guiDC_Model.createLDC"
        double[][] xyRanks = DoubleArray.weibullPlottingPosition(sortedData_combined);
        double[][] xyRanks_period1 = DoubleArray.weibullPlottingPosition(period1Data);
        double[][] xyRanks_period2 = DoubleArray.weibullPlottingPosition(period2Data);
        double[][] xyRanks_period3 = DoubleArray.weibullPlottingPosition(period3Data);
        
        //Determine summary table from xyRanks for duration curve
        double[][] loadRanks = new double[xyRanks.length][2];
        double[][] loadRanks_period1 = new double[xyRanks_period1.length][2];
        double[][] loadRanks_period2 = new double[xyRanks_period2.length][2];
        double[][] loadRanks_period3 = new double[xyRanks_period3.length][2];
        for(int i=0; i<xyRanks.length; i++){
            loadRanks[i][0] = xyRanks[i][0];
            loadRanks[i][1] = xyRanks[i][1] * conversion * wqTarget;
            if(i < xyRanks_period1.length){
                loadRanks_period1[i][0] = xyRanks_period1[i][0];
                loadRanks_period1[i][1] = xyRanks_period1[i][1] * conversion * wqTarget;
            }
            if(i < xyRanks_period2.length){
                loadRanks_period2[i][0] = xyRanks_period2[i][0];
                loadRanks_period2[i][1] = xyRanks_period2[i][1] * conversion * wqTarget;
            }
            if(i < xyRanks_period3.length){
                loadRanks_period3[i][0] = xyRanks_period3[i][0];
                loadRanks_period3[i][1] = xyRanks_period3[i][1] * conversion * wqTarget;
            }
        }
        calculatePercentileSummmary(loadRanks, loadRanks_period1, loadRanks_period2, loadRanks_period3, "Load (" + endUnits +")");

        //Break scattered WQ points data into the 5 flow sub classifications 0-10, 10-40, 40-60, 60-90, 90-100
        //which are High flows, Moist conditions, Mid-range flows, Dry conditions, and low flows respectively Assimilative
        String yaxis_title = WQlabel + " [" + endUnits + "]";
        XYSeries LDC_xy = new XYSeries("LDC");
        XYSeries LDC_user = new XYSeries("LDC User Data");
        XYSeries LDC_xy_period1 = new XYSeries("Period 1 LDC: " + period1Begin + " to " + period1End);
        XYSeries LDC_xy_period2 = new XYSeries("Period 2 LDC: " + period1Begin + " to " + period1End);
        XYSeries LDC_xy_period3 = new XYSeries("Period 3 LDC: " + period1Begin + " to " + period1End);
        XYSeries WQ_pts = new XYSeries("Water Quality Obs.");
        XYSeries WQ_pts_user = new XYSeries("Water Quality Obs. User Data");
        XYSeries Seasonal_WQ_pts = new XYSeries("Seasonal Water Quality Obs.");
        XYSeries Seasonal_WQ_pts_user = new XYSeries("Seasonal Water Quality Obs. User Data");
        XYSeries mQn_xy = new XYSeries("Current " + String.valueOf(m) + "Q" + String.valueOf(n) + " water quality standard");
        String[][] graphData = new String[xyRanks.length][8];
        int ctr = 0;
        for (int i=0; i<xyRanks.length; i++){
            double y = xyRanks[i][1] * conversion * wqTarget;
            double x = xyRanks[i][0];
            LDC_xy.add(x, y);
            graphData[i][0] = String.valueOf(x);
            graphData[i][1] = String.valueOf(y);
            if(y > mQn){
                mQn_xy.add(x,mQn);
            }else{
                mQn_xy.add(x, y);
            }
            //Create an XYSeries only if userdata is not empty
            if(sortedData_user.length > 0 && sortedData_user.length > ctr){
                double userValue = Double.parseDouble(sortedData_user[ctr][1]);
                if(Double.compare(xyRanks[i][1], userValue) == 0){
                    LDC_user.add(x, y);
                    ctr++;
                }
            }
            //Create period 1 line
            if(i < xyRanks_period1.length){
                LDC_xy_period1.add(xyRanks_period1[i][0], xyRanks_period1[i][1] * conversion * wqTarget);//x,y
                graphData[i][2] = String.valueOf(xyRanks_period1[i][0]);
                graphData[i][3] = String.valueOf(xyRanks_period1[i][1] * conversion * wqTarget);
            }else{
                graphData[i][2] = "-1";
                graphData[i][3] = "-1";
            }
            //Create period 2 line
            if(i < xyRanks_period2.length){
                LDC_xy_period2.add(xyRanks_period2[i][0], xyRanks_period2[i][1] * conversion * wqTarget);//x,y
                graphData[i][4] = String.valueOf(xyRanks_period2[i][0]);
                graphData[i][5] = String.valueOf(xyRanks_period2[i][1] * conversion * wqTarget);
            }else{
                graphData[i][4] = "-1";
                graphData[i][5] = "-1";
            }
            //Create period 3 line
            if(i < xyRanks_period3.length){
                LDC_xy_period3.add(xyRanks_period3[i][0], xyRanks_period3[i][1] * conversion * wqTarget);//x,y
                graphData[i][6] = String.valueOf(xyRanks_period3[i][0]);
                graphData[i][7] = String.valueOf(xyRanks_period3[i][1] * conversion * wqTarget);
            }else{
                graphData[i][6] = "-1";
                graphData[i][7] = "-1";
            }
        }
        //Add WQ test data to graph series and generate box plot data
        ArrayList<Double> high_data = new ArrayList<>();
        ArrayList<Double> moist_data = new ArrayList<>();
        ArrayList<Double> mid_data = new ArrayList<>();
        ArrayList<Double> dry_data = new ArrayList<>();
        ArrayList<Double> low_data = new ArrayList<>();
        double highexceed_count = 0, moistexceed_count =0, midexceed_count =0, dryexceed_count =0,	
                lowexceed_count =0, totalexceed_count =1, totalCount = 0;
        double WQ_load = 0, Seasonal_WQ_load=0;
        ctr = 0;
        for(int i=0;i<WQdata_combined.length;i++){
            for(int j=0; j<sortedData_combined.length;j++){
                if(WQdata_combined[i][0].equalsIgnoreCase(sortedData_combined[j][0])){
                    totalCount = totalCount+1;
                    //Create an XYSeries for WQ points
                    double tempFlowValue = Double.parseDouble(sortedData_combined[j][1]);
                    double tempWQValue = Double.parseDouble(WQdata_combined[i][1]);
                    WQ_load = tempFlowValue * conversion * tempWQValue;
                    
                    //Find what exceedance rank matches with which flow rate
                    double xyRankValue = xyRanks[j][0];
                    double xyLoadValue = loadRanks[j][1]; //Same formate/order as xyRanks but flow was converted to a load value
                    for(int k=0; k<xyRanks.length; k++){
                        if(xyRanks[k][1] == Double.parseDouble(sortedData_combined[j][1])){
                           xyRankValue = xyRanks[k][0];
                           xyLoadValue = loadRanks[k][1];
                           break;
                        }
                    }                    
                    WQ_pts.add(xyRankValue,WQ_load);

                    //Create an XYSeries only if user WQ data is not empty
                    if(WQdata_user.length > 0 && WQdata_user.length > ctr){
                        if(WQdata_user[ctr][0].equalsIgnoreCase(sortedData_combined[j][0])){
                            WQ_pts_user.add(xyRankValue,WQ_load);
                            ctr++;
                        }
                    }

                    //Also add to category box plot data
                    if((xyRankValue <= 10) & (xyRankValue >= 0)){
                        high_data.add(WQ_load);
                        if(WQ_load > xyLoadValue){
                            highexceed_count++;
                        }
                    }
                    if((xyRankValue <= 40) & (xyRankValue > 10)){
                        moist_data.add(WQ_load);
                        if(WQ_load > xyLoadValue){
                            moistexceed_count++;
                        }
                    }
                    if((xyRankValue <= 60) & (xyRankValue > 40)){
                        mid_data.add(WQ_load);
                        if(WQ_load > xyLoadValue){
                            midexceed_count++;
                        }
                    }
                    if((xyRankValue <= 90) & (xyRankValue > 60)){
                        dry_data.add(WQ_load);
                        if(WQ_load > xyLoadValue){
                            dryexceed_count++;
                        }
                    }
                    if((xyRankValue <= 100) & (xyRankValue > 90)){
                        low_data.add(WQ_load);
                        if(WQ_load > xyLoadValue){
                            lowexceed_count++;
                        }
                    }
                    for(int k=0;k<seasonalWQ_combined.length;k++){
                        if(seasonalWQ_combined[k][0].equalsIgnoreCase(sortedData_combined[j][0])){
                            double tempSeasonalWQValue = Double.parseDouble(seasonalWQ_combined[k][1]);
                            Seasonal_WQ_load = (Double.parseDouble(sortedData_combined[j][1]))*conversion*tempSeasonalWQValue;
                            Seasonal_WQ_pts.add(xyRankValue,Seasonal_WQ_load);
                        }
                    }
                    for(int k=0;k<seasonalWQ_user.length;k++){
                        if(seasonalWQ_user[k][0].equalsIgnoreCase(sortedData_combined[j][0])){
                            double tempSeasonalWQValue = Double.parseDouble(seasonalWQ_user[k][1]);
                            Seasonal_WQ_load = (Double.parseDouble(sortedData_combined[j][1]))*conversion*tempSeasonalWQValue;
                            Seasonal_WQ_pts_user.add(xyRankValue,Seasonal_WQ_load);
                        }
                    }
                    break;
                }
            }
        }

        //Determine which pollution source is likely based on location of exceeded water quality tests.
        totalexceed_count = highexceed_count + moistexceed_count + midexceed_count + dryexceed_count + lowexceed_count;
        if (totalexceed_count == 0){
            totalexceed_count = 1;
        }
        //Takes the counts of exceedance points and makes them relative to the total count of exceedances.
        highexceed_count = highexceed_count/totalexceed_count;
        moistexceed_count = moistexceed_count/totalexceed_count;
        midexceed_count = midexceed_count/totalexceed_count;
        dryexceed_count = dryexceed_count/totalexceed_count;
        lowexceed_count = lowexceed_count/totalexceed_count;

        //Pollution Source paragraph is only for nutrient tests so check if the current test is for nutrients:
        boolean nutrient = false;
        if (wqCode.equalsIgnoreCase("00597") || wqCode.equalsIgnoreCase("00600") || wqCode.equalsIgnoreCase("00601") || wqCode.equalsIgnoreCase("00602") || 
                wqCode.equalsIgnoreCase("00604") || wqCode.equalsIgnoreCase("00605") || wqCode.equalsIgnoreCase("00606") || wqCode.equalsIgnoreCase("00607") || 
                wqCode.equalsIgnoreCase("00608") || wqCode.equalsIgnoreCase("00610") || wqCode.equalsIgnoreCase("00613") || wqCode.equalsIgnoreCase("00615") || 
                wqCode.equalsIgnoreCase("00618") || wqCode.equalsIgnoreCase("00619") || wqCode.equalsIgnoreCase("00620") || wqCode.equalsIgnoreCase("00623") || 
                wqCode.equalsIgnoreCase("00624") || wqCode.equalsIgnoreCase("00625") || wqCode.equalsIgnoreCase("00628") || wqCode.equalsIgnoreCase("00630") || 
                wqCode.equalsIgnoreCase("00631") || wqCode.equalsIgnoreCase("00635") || wqCode.equalsIgnoreCase("00636") || wqCode.equalsIgnoreCase("00639") || 
                wqCode.equalsIgnoreCase("00650") || wqCode.equalsIgnoreCase("00653") || wqCode.equalsIgnoreCase("00660") || wqCode.equalsIgnoreCase("00665") || 
                wqCode.equalsIgnoreCase("00666") || wqCode.equalsIgnoreCase("00667") || wqCode.equalsIgnoreCase("00669") || wqCode.equalsIgnoreCase("00670") || 
                wqCode.equalsIgnoreCase("00671") || wqCode.equalsIgnoreCase("00672") || wqCode.equalsIgnoreCase("00673") || wqCode.equalsIgnoreCase("00674") || 
                wqCode.equalsIgnoreCase("00675") || wqCode.equalsIgnoreCase("00676") || wqCode.equalsIgnoreCase("00677") || wqCode.equalsIgnoreCase("00678") || 
                wqCode.equalsIgnoreCase("01425") || wqCode.equalsIgnoreCase("01465") || wqCode.equalsIgnoreCase("49567") || wqCode.equalsIgnoreCase("49570") || 
                wqCode.equalsIgnoreCase("62854") || wqCode.equalsIgnoreCase("62855") || wqCode.equalsIgnoreCase("64832") || wqCode.equalsIgnoreCase("70507") || 
                wqCode.equalsIgnoreCase("71845") || wqCode.equalsIgnoreCase("71846") || wqCode.equalsIgnoreCase("71850") || wqCode.equalsIgnoreCase("71851") || 
                wqCode.equalsIgnoreCase("71855") || wqCode.equalsIgnoreCase("71856") || wqCode.equalsIgnoreCase("71886") || wqCode.equalsIgnoreCase("71887") || 
                wqCode.equalsIgnoreCase("71888") || wqCode.equalsIgnoreCase("76008") || wqCode.equalsIgnoreCase("76009") || wqCode.equalsIgnoreCase("76010") || 
                wqCode.equalsIgnoreCase("82046") || wqCode.equalsIgnoreCase("83044") || wqCode.equalsIgnoreCase("83047") || wqCode.equalsIgnoreCase("83050") || 
                wqCode.equalsIgnoreCase("83053") || wqCode.equalsIgnoreCase("83056") || wqCode.equalsIgnoreCase("83059") || wqCode.equalsIgnoreCase("83062") ||
                wqCode.equalsIgnoreCase("83065") || wqCode.equalsIgnoreCase("83068") || wqCode.equalsIgnoreCase("83071") || wqCode.equalsIgnoreCase("83074") || 
                wqCode.equalsIgnoreCase("83077") || wqCode.equalsIgnoreCase("83080") || wqCode.equalsIgnoreCase("83083") || wqCode.equalsIgnoreCase("83086") || 
                wqCode.equalsIgnoreCase("83089") || wqCode.equalsIgnoreCase("83092") || wqCode.equalsIgnoreCase("83095") || wqCode.equalsIgnoreCase("83098") || 
                wqCode.equalsIgnoreCase("83101") || wqCode.equalsIgnoreCase("83108") || wqCode.equalsIgnoreCase("83111") || wqCode.equalsIgnoreCase("83114") || 
                wqCode.equalsIgnoreCase("83117") || wqCode.equalsIgnoreCase("83326") || wqCode.equalsIgnoreCase("83329") || wqCode.equalsIgnoreCase("83332") || 
                wqCode.equalsIgnoreCase("83335") || wqCode.equalsIgnoreCase("83338") || wqCode.equalsIgnoreCase("83341") || wqCode.equalsIgnoreCase("83344") || 
                wqCode.equalsIgnoreCase("83347") || wqCode.equalsIgnoreCase("83350") || wqCode.equalsIgnoreCase("83353") || wqCode.equalsIgnoreCase("83356") || 
                wqCode.equalsIgnoreCase("83359") || wqCode.equalsIgnoreCase("83362") || wqCode.equalsIgnoreCase("83365") || wqCode.equalsIgnoreCase("83368") || 
                wqCode.equalsIgnoreCase("83371") || wqCode.equalsIgnoreCase("83374") || wqCode.equalsIgnoreCase("83377") || wqCode.equalsIgnoreCase("83380") || 
                wqCode.equalsIgnoreCase("83383") || wqCode.equalsIgnoreCase("83390") || wqCode.equalsIgnoreCase("83393") || wqCode.equalsIgnoreCase("83396") || 
                wqCode.equalsIgnoreCase("83399") || wqCode.equalsIgnoreCase("90859") || wqCode.equalsIgnoreCase("91003") || wqCode.equalsIgnoreCase("91004") || 
                wqCode.equalsIgnoreCase("99116") || wqCode.equalsIgnoreCase("99120") || wqCode.equalsIgnoreCase("99121") || wqCode.equalsIgnoreCase("99122") || 
                wqCode.equalsIgnoreCase("99123") || wqCode.equalsIgnoreCase("99124") || wqCode.equalsIgnoreCase("99125") || wqCode.equalsIgnoreCase("99126") || 
                wqCode.equalsIgnoreCase("99133") || wqCode.equalsIgnoreCase("99410") || wqCode.equalsIgnoreCase("99411") || wqCode.equalsIgnoreCase("99412") || 
                wqCode.equalsIgnoreCase("99413") || wqCode.equalsIgnoreCase("99414") || wqCode.equalsIgnoreCase("99415") || wqCode.equalsIgnoreCase("99416") || 
                wqCode.equalsIgnoreCase("99889") || wqCode.equalsIgnoreCase("99891") || wqCode.equalsIgnoreCase("99892") || wqCode.equalsIgnoreCase("99893") || 
                wqCode.equalsIgnoreCase("99894")){
            nutrient = true;
        }

        //Decide based on properties of the data which pollution source is likely and display the paragraph 
        //about that pollution source.
        String paragraphTitle = "";
        if (nutrient == true && (lowexceed_count > 0 || dryexceed_count > 0) && 
                (midexceed_count < 0.1 && moistexceed_count < 0.075 && highexceed_count < 0.05) &&
                ((totalexceed_count/totalCount) > 0.15 || totalexceed_count > 5)){
            paragraphTitle = "Point Sources and Wastewater Sources: ";
            //If the pollutant target is exceeded under low flows, it is probably a "point source".
        }else if (nutrient == true && (moistexceed_count > 0 && midexceed_count > 0) && 
                (dryexceed_count < 0.1 && dryexceed_count > 0 && lowexceed_count < 0.05) && 
                ((totalexceed_count/totalCount) > 0.15 || totalexceed_count > 5)){
            paragraphTitle = "Upper Flow Sources: ";
            //If the pollutant target is exceeded primarily beginning around 55% flow interval, 
            //it is probably an "upper-flow" driven non-point-source pollution.
        }else if (nutrient == true && (moistexceed_count > 0 && midexceed_count > 0 && dryexceed_count > 0 && lowexceed_count < 0.12) && 
                (moistexceed_count < 0.12 && midexceed_count < 0.12 && dryexceed_count < 0.1) && 
                ((totalexceed_count/totalCount) > 0.15 || totalexceed_count > 5)){
            paragraphTitle = "Wet-Weather Sources: ";
            //If the pollutant target is exceeded primarily beginning around 70% flow interval, 
            //it is probably a "wet-weather" driven non-point-source pollution.
        }else if (nutrient == true && (highexceed_count > 0 || (highexceed_count > 0 && moistexceed_count > 0)) && 
                midexceed_count < 0.2 && 
                dryexceed_count < 0.1 && 
                lowexceed_count < 0.1 &&
                ((totalexceed_count/totalCount) > 0.15 || totalexceed_count > 5)){
            paragraphTitle = "Erosion Sources: ";
            //If the pollutant target is exceeded primarily in the high flow interval, 
            //"erosion" processes are the likely pollution source.
        }else if (nutrient == true && (dryexceed_count > 0.1 && midexceed_count > 0.1 && moistexceed_count > 0.1) || 
                (totalexceed_count/totalCount) > 0.5){
            paragraphTitle = "Multiple Pollution Sources: ";
            //if the target is almost always exceeded in most of the flow intervals,  
            //display that multiple sources are likely.
        }else{
            paragraphTitle = "No Apparent Pollution Source: ";
            //If there is no easily identified pollution source, display that information along 
            //with instructions to get to more information.
        }

        //Create a graph with the LDC (line) and WQ points (scatter)
        XYPlot plot = new XYPlot();
        XYDataset LDC_data = new XYSeriesCollection(LDC_xy);
        XYItemRenderer renderer1 = new XYLineAndShapeRenderer(true, false);
        int seriesIndex = 0;
        
        //Create X Axis
        ValueAxis xAxis = new NumberAxis("Percent of Time Load is Exceeded [%]");
        xAxis.setRange(0,100);
        plot.setDomainAxis(0, xAxis);

        //Set log-scale Y axis with scientific notation
        LogarithmicAxis yAxis = new LogarithmicAxis(yaxis_title);
        yAxis.setAllowNegativesFlag(true);
        yAxis.setLog10TickLabelsFlag(true);
        plot.setRangeAxis(0, yAxis);
        //Put the LDC line data, renderer, and axis into plot
        plot.setDataset(seriesIndex, LDC_data);
        plot.setRenderer(seriesIndex, renderer1);
        //Put the line on the first Domain and first Range
        plot.mapDatasetToDomainAxis(seriesIndex, 0);
        plot.mapDatasetToRangeAxis(seriesIndex, 0);
        seriesIndex++;
        
        //Graph period 1 data
        if(period1Data.length > 0){
            XYDataset LDC_period = new XYSeriesCollection(LDC_xy_period1);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false);
            renderer.setSeriesPaint(0, new Color(255, 135, 0));//gold
            plot.setDataset(seriesIndex, LDC_period);
            plot.setRenderer(seriesIndex, renderer);
            plot.mapDatasetToDomainAxis(seriesIndex, 0);
            plot.mapDatasetToRangeAxis(seriesIndex, 0);
            seriesIndex++;
        }
        //Graph period 2 data
        if(period2Data.length > 0){
            XYDataset LDC_period = new XYSeriesCollection(LDC_xy_period2);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false);
            renderer.setSeriesPaint(0, Color.green);
            plot.setDataset(seriesIndex, LDC_period);
            plot.setRenderer(seriesIndex, renderer);
            plot.mapDatasetToDomainAxis(seriesIndex, 0);
            plot.mapDatasetToRangeAxis(seriesIndex, 0);
            seriesIndex++;
        }
        //Graph period 3 data
        if(period3Data.length > 0){
            XYDataset LDC_period = new XYSeriesCollection(LDC_xy_period3);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false);
            renderer.setSeriesPaint(0, Color.cyan);
            plot.setDataset(seriesIndex, LDC_period);
            plot.setRenderer(seriesIndex, renderer);
            plot.mapDatasetToDomainAxis(seriesIndex, 0);
            plot.mapDatasetToRangeAxis(seriesIndex, 0);
            seriesIndex++;
        }

        //Create mQn Line
        if(mQn != 0){//only show mQn if it is not zero
            XYDataset mQn_data = new XYSeriesCollection(mQn_xy);
            XYItemRenderer renderer2 = new XYLineAndShapeRenderer(true, false);
            renderer2.setSeriesPaint(0, Color.black);
            plot.setDataset(seriesIndex,mQn_data);
            plot.setRenderer(seriesIndex,renderer2);
            plot.mapDatasetToDomainAxis(seriesIndex,0);
            plot.mapDatasetToRangeAxis(seriesIndex,0);
            seriesIndex++;
        }

        //Create user data points
        if(sortedData_user.length != 0){//only show user points if it is not zero
            //User LDC line
            XYDataset user_data = new XYSeriesCollection(LDC_user);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(false, true);
            renderer.setSeriesPaint(0, Color.red);
            plot.setDataset(seriesIndex,user_data);
            plot.setRenderer(seriesIndex,renderer);
            plot.mapDatasetToDomainAxis(seriesIndex,0);
            plot.mapDatasetToRangeAxis(seriesIndex,0);
            seriesIndex++;
        }
        if(seasonalWQ_user.length != 0){//only show user points if it is not zero
            //User seasonal WQ points
            XYDataset user_seasonal_wq_data = new XYSeriesCollection(Seasonal_WQ_pts_user);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(false, true);
            renderer.setSeriesShape(0, new Rectangle2D.Double(-1.5, 4.5, 3.0, 3.0));
            renderer.setSeriesPaint(0, Color.gray);
            plot.setDataset(seriesIndex,user_seasonal_wq_data);
            plot.setRenderer(seriesIndex,renderer);
            plot.mapDatasetToDomainAxis(seriesIndex,0);
            plot.mapDatasetToRangeAxis(seriesIndex,0);
            seriesIndex++;
        }
        if(WQdata_user.length != 0){//only show user points if it is not zero
            //User WQ points
            XYDataset user_wq_data = new XYSeriesCollection(WQ_pts_user);
            XYItemRenderer renderer = new XYLineAndShapeRenderer(false, true);
            renderer.setSeriesShape(0, new Rectangle2D.Double(-3.0, 3.0, 6.0, 6.0));
            renderer.setSeriesPaint(0, new Color(105, 0, 255));
            plot.setDataset(seriesIndex,user_wq_data);
            plot.setRenderer(seriesIndex,renderer);
            plot.mapDatasetToDomainAxis(seriesIndex,0);
            plot.mapDatasetToRangeAxis(seriesIndex,0);
            seriesIndex++;
        }

        //Graph the seasonal scatter wq data
        XYDataset scatter_data4 = new XYSeriesCollection(Seasonal_WQ_pts);
        XYItemRenderer renderer4 = new XYLineAndShapeRenderer(false, true);
        renderer4.setSeriesShape(0, new Rectangle2D.Double(-1.5, 4.5, 3.0, 3.0));
        renderer4.setSeriesPaint(0, Color.black); 
        plot.setDataset(seriesIndex, scatter_data4);
        plot.setRenderer(seriesIndex, renderer4);
        plot.mapDatasetToDomainAxis(seriesIndex, 0);
        plot.mapDatasetToRangeAxis(seriesIndex, 0);
        seriesIndex++;
        
        //Graph the complete scatter wq data
        XYDataset scatter_data3 = new XYSeriesCollection(WQ_pts);
        XYItemRenderer renderer3 = new XYLineAndShapeRenderer(false, true);
        renderer3.setSeriesShape(0, new Rectangle2D.Double(-3.0, 3.0, 6.0, 6.0));
        renderer3.setSeriesPaint(0, Color.green);
        plot.setDataset(seriesIndex, scatter_data3);
        plot.setRenderer(seriesIndex, renderer3);
        plot.mapDatasetToDomainAxis(seriesIndex, 0);
        plot.mapDatasetToRangeAxis(seriesIndex, 0);
        seriesIndex++;

        //Create box plot of WQ points
        boolean showOutliers = false, showExtremeOutliers = false;
        if(high_data.size() > 4){
            //Create quartile rectangle, min-max line, and median line
            Object[] returnArray = Graphing.boxplot_shapes(plot, 5, high_data, seriesIndex, showOutliers, showExtremeOutliers);
            plot = (XYPlot) returnArray[0];
            showOutliers = (Boolean) returnArray[1];
            showExtremeOutliers = (Boolean) returnArray[2];
            seriesIndex = (Integer) returnArray[3];
        }

        if(moist_data.size() > 4){
            //Create quartile rectangle, min-max line, and median line
            Object[] returnArray = Graphing.boxplot_shapes(plot, 25, moist_data, seriesIndex, showOutliers, showExtremeOutliers);
            plot = (XYPlot) returnArray[0];
            showOutliers = (Boolean) returnArray[1];
            showExtremeOutliers = (Boolean) returnArray[2];
            seriesIndex = (Integer) returnArray[3];
        }

        if(mid_data.size() > 4){
            //Create quartile rectangle, min-max line, and median line
            Object[] returnArray = Graphing.boxplot_shapes(plot, 50, mid_data, seriesIndex, showOutliers, showExtremeOutliers);
            plot = (XYPlot) returnArray[0];
            showOutliers = (Boolean) returnArray[1];
            showExtremeOutliers = (Boolean) returnArray[2];
            seriesIndex = (Integer) returnArray[3];
        }

        if(dry_data.size() > 4){
            //Create quartile rectangle, min-max line, and median line
            Object[] returnArray = Graphing.boxplot_shapes(plot, 75, dry_data, seriesIndex, showOutliers, showExtremeOutliers);
            plot = (XYPlot) returnArray[0];
            showOutliers = (Boolean) returnArray[1];
            showExtremeOutliers = (Boolean) returnArray[2];
            seriesIndex = (Integer) returnArray[3];
        }

        if(low_data.size() > 4){
            //Create quartile rectangle, min-max line, and median line
            Object[] returnArray = Graphing.boxplot_shapes(plot, 95, low_data, seriesIndex, showOutliers, showExtremeOutliers);
            plot = (XYPlot) returnArray[0];
            showOutliers = (Boolean) returnArray[1];
            showExtremeOutliers = (Boolean) returnArray[2];
            seriesIndex = (Integer) returnArray[3];
        }
        
        //Reformat WQ and Seasonal WQ data for use with JHighCharts
        String[] partialWQdata_x = new String[xyRanks.length];
        String[] partialWQdata_y = new String[xyRanks.length];
        String[] partialSeasonalWQdata_x = new String[xyRanks.length];
        String[] partialSeasonalWQdata_y = new String[xyRanks.length];
        for(int i=0; i<xyRanks.length; i++){
            //Try WQ data
            partialWQdata_x[i] = "-1";//value
            partialWQdata_y[i] = "-1";//value
            partialSeasonalWQdata_x[i] = "-1";//value
            partialSeasonalWQdata_y[i] = "-1";//value
            try{
                
                partialWQdata_x[i] = String.valueOf(WQ_pts.getX(i));//x
                partialWQdata_y[i] = String.valueOf(WQ_pts.getY(i));//y
            }catch(IndexOutOfBoundsException e){
                //do nothing as it already has a -1 value
            }
            //Try Seasonal WQ data
            try{
                partialSeasonalWQdata_x[i] = String.valueOf(Seasonal_WQ_pts.getX(i));//x
                partialSeasonalWQdata_y[i] = String.valueOf(Seasonal_WQ_pts.getY(i));//y
            }catch(IndexOutOfBoundsException e){
                //do nothing as it already has a -1 value
            }
        }
        graphData = DoubleArray.appendcolumn_Matrix(graphData, partialSeasonalWQdata_x);//Add Seasonal WQ x points
        graphData = DoubleArray.appendcolumn_Matrix(graphData, partialSeasonalWQdata_y);//Add Seasonal WQ y points
        graphData = DoubleArray.appendcolumn_Matrix(graphData, partialWQdata_x);//Add WQ x points
        graphData = DoubleArray.appendcolumn_Matrix(graphData, partialWQdata_y);//Add WQ y points

        //Graph a LDC for each year in time period
        String currentYear = start.substring(0,4);
        String finalYear = end.substring(0,4);
        boolean moreYears = xyRanks.length > 0;
        while(moreYears){
            //Get current years data and graph it
            String[][] partialData = DoubleArray.getYearsData(sortedData_combined, currentYear);
            double[][] partialRanks = DoubleArray.weibullPlottingPosition(partialData);
            for(int i=0; i<partialRanks.length; i++){
                partialRanks[i][1] = partialRanks[i][1] * conversion * wqTarget;
            }
            plot = Graphing.addXYSeries(plot, partialRanks, Color.lightGray, seriesIndex);
            seriesIndex++;
            
            //Save results for output for JHighCharts
            String[] partialLDCData_x = new String[xyRanks.length];
            String[] partialLDCData_y = new String[xyRanks.length];
            for(int i=0; i<xyRanks.length; i++){
                partialLDCData_x[i] = "-1";//value
                partialLDCData_y[i] = "-1";//value
                try{
                    partialLDCData_x[i] = String.valueOf(partialRanks[i][0]);//x
                    partialLDCData_y[i] = String.valueOf(partialRanks[i][1]);//y
                }catch(IndexOutOfBoundsException e){
                    //do nothing as it already has a -1 value
                }
            }
            graphData = DoubleArray.appendcolumn_Matrix(graphData, partialLDCData_x);//Add x points
            graphData = DoubleArray.appendcolumn_Matrix(graphData, partialLDCData_y);//Add y points
            
            //Determine the next data year to continue looping over
            int nextYear = Integer.parseInt(currentYear) + 1;
            if(finalYear.compareToIgnoreCase(String.valueOf(nextYear)) >= 0){
                currentYear = String.valueOf(nextYear);
            }else{
                moreYears = false;
            }
        }
        
        //Output XY data for use with JHighCharts
        DoubleArray.writeXYseries(directory, graphData, getDCgraphOutput().getName());

        //Add Flow Range Labels
        plot = Graphing.addIntervalLabel(plot, "High Flow", 0, 10);
        plot = Graphing.addIntervalLabel(plot, "Moist Conditions", 10, 40);
        plot = Graphing.addIntervalLabel(plot, "Mid-Range Flows", 40, 60);
        plot = Graphing.addIntervalLabel(plot, "Dry Conditions", 60, 90);
        plot = Graphing.addIntervalLabel(plot, "Low Flow", 90, 100);

        //Set extra plot preferences
        plot = Graphing.setLogYaxisPreferences(plot);

        //Graph plot onto JfreeChart
        String graphTitle = "Load Duration Curve for " + database + " Station " + stationId + "; " + stationName;
        JFreeChart chart = new JFreeChart(graphTitle, Graphing.titleFont, plot, true);

        //Set legend Font
        LegendTitle legendTitle = chart.getLegend();
        legendTitle.setItemFont(Graphing.masterFont);

        //Write a results file containing the flow duration curve interval (exceedance values) and their corresponding discharge values
        writeResults(graphData, yaxis_title, true);
        
        //Save resulting graph
        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.out.println("A problem occurred while trying to creating the chart for station " + stationName + ". Error: USGSLDC0003");
            System.out.println(e.toString());
        }

        Object[] returnArray = {paragraphTitle, totalCount};
        return returnArray;
    }
    /**
     * Main statistics function calls other functions to calculate each statistic value then stores the results as global variables
     * @param dataList  data on which statistical values are desired
     */
    private void CalculateStatistics(String[][] data) {
        //Convert into double list
        ArrayList<Double> dataList = new ArrayList<>();
        for(int i=0; i<data.length; i++){
            dataList.add(Double.parseDouble(data[i][1]));
        }
        
        //Calculate statistics
        flowMedian = DoubleMath.round(DoubleMath.median(dataList),3);//Call Median function
        flowMean = DoubleMath.round(DoubleMath.meanArithmetic(dataList),3);//Call Mean function
    }
    /**
     * Calculates a summary table of standardized percentiles based on the data
     * @param xyRanks  a double[][] array with column1 = x (0-100 percentages), column2 = values
     * @param xyRanks_period1  a double[][] array with column1 = x (0-100 percentages), column2 = values
     * @param xyRanks_period2  a double[][] array with column1 = x (0-100 percentages), column2 = values
     * @param xyRanks_period3  a double[][] array with column1 = x (0-100 percentages), column2 = values
     * @param title 
     */
    private void calculatePercentileSummmary(double[][] xyRanks, double[][] xyRanks_period1, double[][] xyRanks_period2, double[][] xyRanks_period3, String title){
        //Hard coded percentile summary table
        double[] frqDisp = {99, 95, 90, 75, 50, 25, 10, 5, 1};
        String[] frqDispString = {"99", "95", "90", "75", "50", "25", "10", "5", "1"};
        
        //Interpolate desired percentiles
        double[] yFinal = DoubleMath.linearInterpolation(DoubleArray.getColumn(xyRanks,0), DoubleArray.getColumn(xyRanks,1), frqDisp);
        double[] period1 = {-1, -1, -1, -1, -1, -1, -1, -1, -1};
        double[] period2 = {-1, -1, -1, -1, -1, -1, -1, -1, -1};
        double[] period3 = {-1, -1, -1, -1, -1, -1, -1, -1, -1};
        if(xyRanks_period1.length > 0){
            period1 = DoubleMath.linearInterpolation(DoubleArray.getColumn(xyRanks_period1,0), DoubleArray.getColumn(xyRanks_period1,1), frqDisp);
        }
        if(xyRanks_period2.length > 0){
            period2 = DoubleMath.linearInterpolation(DoubleArray.getColumn(xyRanks_period2,0), DoubleArray.getColumn(xyRanks_period2,1), frqDisp);
        }
        if(xyRanks_period3.length > 0){
            period3 = DoubleMath.linearInterpolation(DoubleArray.getColumn(xyRanks_period3,0), DoubleArray.getColumn(xyRanks_period3,1), frqDisp);
        }

        //Round results for summary table
        yFinal = DoubleMath.roundColumn(yFinal, 3);
        period1 = DoubleMath.roundColumn(period1, 3);
        period2 = DoubleMath.roundColumn(period2, 3);
        period3 = DoubleMath.roundColumn(period3, 3);

        //Create summary table in a single string
        summaryTable = "Exceedance Percentile\t" + title + "\tPeriod 1 " + title + "\tPeriod 2 " + title + "\tPeriod 3 " + title;
        for(int i=0; i<frqDisp.length; i++){
            summaryTable = summaryTable + "\n"
                    + frqDispString[i] + "\t"
                    + String.valueOf(yFinal[i])+ "\t"
                    + String.valueOf(period1[i])+ "\t"
                    + String.valueOf(period2[i])+ "\t"
                    + String.valueOf(period3[i]);
        }
    }
    /**
     * Writes out the non-zero results of the duration curve analysis which are the flow duration curve intervale and their corresponding flow/load values
     * @param results  double[][] array to be written as each line of the text file
     * @param yColumnHeader  the header data label for y column data (i.e. flow or load)
     * @param ldcTF  true if load duration curve, false otherwise
     * @throws IOException
     */
    private void writeResults(String[][] results, String yColumnHeader, boolean ldcTF) throws IOException{
        //Combine column data
        ArrayList<String> finalResults = new ArrayList<>();
        for(int i=0; i<results.length; i++){
            String currentRow = results[i][0];
            for(int j=1; j<results[i].length; j++){
                currentRow += "\t" + results[i][j];
            }
            finalResults.add(currentRow);
        }
        
        //Create header
        int currentYear = Integer.parseInt(start.substring(0,4));
        String header = null;
        boolean xDataTF = true;
        for(int j=0; j<results[0].length; j++){
            switch (j) {
                case 0:
                    header = "Percent of Time Flow is Exceeded [%] (overall)";
                    break;
                case 1:
                    header += "\t" + yColumnHeader + " (overall)";
                    break;
                case 2:
                    header += "\t" + "Exceedance (Period 1: " + period1Begin + " to " + period1End + ")";
                    break;
                case 3:
                    header += "\t" + yColumnHeader + " (Period 1: " + period1Begin + " to " + period1End + ")";
                    break;
                case 4:
                    header += "\t" + "Exceedance (Period 2: " + period2Begin + " to " + period2End + ")";
                    break;
                case 5:
                    header += "\t" + yColumnHeader + " (Period 2: " + period2Begin + " to " + period2End + ")";
                    break;
                case 6:
                    header += "\t" + "Exceedance (Period 3: " + period3Begin + " to " + period3End + ")";
                    break;
                case 7:
                    header += "\t" + yColumnHeader + " (Period 3: " + period3Begin + " to " + period3End + ")";
                    break;
                default:
                    if(xDataTF){
                        if(ldcTF && j == 8){
                            header += "\t" + "Exceedance (Seasonal Water Quality Obs.)";
                        }else if(ldcTF && j == 10){
                            header += "\t" + "Exceedance (Water Quality Obs.)";
                        }else{
                            header += "\t" + "Exceedance (" + currentYear + ")";
                        }
                        xDataTF = false;
                    }else{
                        if(ldcTF && j == 9){
                            header += "\t" + yColumnHeader + " (Seasonal Water Quality Obs.)";
                        }else if(ldcTF && j == 11){
                            header += "\t" + yColumnHeader + " (Water Quality Obs.)";
                        }else{
                            header += "\t" + yColumnHeader + " (" + currentYear + ")";
                            currentYear++;
                        }
                        xDataTF = true;
                    }
                    break;
            }
        }
        
        String path = directory + File.separator + getDurationCurve_results().getName();
        FileWriter writer =  new FileWriter(path, false);
        PrintWriter printLine = new PrintWriter(writer);

        //Add Headers to text file
        printLine.printf("%s" + "\r\n", header);

        //Output data to text file
        for(int i=0; i < finalResults.size(); i++) {
                printLine.printf("%s" + "\r\n", finalResults.get(i));
        }
        System.out.println("Text File located at:\t" + path);
        printLine.close();
        writer.close();
    }
    /**
     * Create the dynamic paragraph requested
     * @param paragraphTitle  title of the desired paragraph  
     * @return  the dynamic paragraph to be displayed to the user
     */
    public String dynamicParagraph(String paragraphTitle) {
        String dynamic_paragraph = paragraphTitle + "\n\n";
        if(paragraphTitle.equals("Point Sources and Wastewater Sources: ")){
            dynamic_paragraph = dynamic_paragraph + "When the target load duration curve is exceeded primarily under Low Flow and Dry Conditions flow intervals; point sources and wastewater source pollution are the likely cause.  Note: in order to have wastewater pollution source there must be an upstream wastewater plant discharging into the watershed.  Solutions to this problem may include but are not limited to: point source controls, septic system inspection programs, and sanitary sewer overflow repair (Cleland 2003).  In urban areas solutions might involve detecting illicit connections from storm water programs (Cleland 2007).  In agricultural settings, solutions may include fencing livestock from riparian areas along waterways, or other similar basic management practices (Cleland 2007).\n";
            dynamic_paragraph = dynamic_paragraph + "The grey graphed lines are duration curves for each individual year within the analysis period.\n";
        }else if(paragraphTitle.equals("Upper Flow Sources: ")){
            dynamic_paragraph = dynamic_paragraph + "Upper flow sources are likely when the target is met except under the High Flows, Moist Conditions, and the upper end of Mid-Range Flows.	The higher flows under these regions, primarily above 55 percent, are likely due to large amounts of storm runoff.  These flows and sources are similar to those of wet-weather, but are due to larger runoff events and higher flows.  The increased pollution may be due to combined sewer overflows, storm water runoff from both up river and nearby, and pasture runoff from agricultural areas (Cleland 2003).  Solutions to this pollutant problem may include but are not limited to: combined sewer overflow repair, pasture management, and up river and nearby storm runoff solutions such as riparian buffers (Cleland 2003). In agricultural areas remediation efforts aimed towards grassed waterways, conservation tillage, pasture management practices, and contour strips may help alleviate these pollutions issues (Cleland 2007).\n";
            dynamic_paragraph = dynamic_paragraph + "The grey graphed lines are duration curves for each individual year within the analysis period.\n";
        }else if(paragraphTitle.equals("Wet-Weather Sources: ")){
            dynamic_paragraph = dynamic_paragraph + "When the target load duration curve for pollution concentration is satisfied under the Low Flow and Dry Conditions flow intervals but becomes exceeded during Mid-Range Flows and Moist Conditions primarily above 70 percent flow interval, wet-weather related pollution sources are likely.  These mid-size flow rates tend to be the result of light storm runoff with increased pollutant transportation through riparian areas and from impervious regions.  Some solutions to these problems may include but are not limited to: riparian buffer zones, agricultural conservation tillage, contour strips, and grassed waterways (Cleland 2007). Combined sewer overflow repair, pet waste ordinances, and hobby farm livestock education may also alleviate the pollution problems due to these wet-weather sources (Cleland 2003).\n";
            dynamic_paragraph = dynamic_paragraph + "The grey graphed lines are duration curves for each individual year within the analysis period.\n";
        }else if(paragraphTitle.equals("Erosion Sources: ")){
            dynamic_paragraph = dynamic_paragraph + "Erosion based pollutant sources are likely when the target is met under all the flow intervals except under the High Flows and upper end of the Moist Conditions. Due to the higher velocities of the water during these flows, bank erosion and channel scour are more likely to occur and release pollutants (particularly sediment) into the water.  Solutions to this pollutant problem may include but are not limited to: river bank stabilization efforts and channel protection policies (Cleland 2007). Pollutants related to sediment concentration will be those most affected by these efforts.\n";
            dynamic_paragraph = dynamic_paragraph + "The grey graphed lines are duration curves for each individual year within the analysis period.\n";
        }else if(paragraphTitle.equals("Multiple Pollution Sources: ")){
            dynamic_paragraph = dynamic_paragraph + "Most of the flow intervals contain many points which exceed the target.  No single pollution source is likely. Please click Further Information for more pollutant identification help.\n";
            dynamic_paragraph = dynamic_paragraph + "The grey graphed lines are duration curves for each individual year within the analysis period.\n";
        }else if(paragraphTitle.equals("Flow Duration Curve Overview: ")){
            dynamic_paragraph = dynamic_paragraph + "A flow duration curve (FDC) is the ranked graphing of river flows on a scale of percent exceedance.  For example a flow value associated with the flow interval of 15% means that particular flow value is met or exceeded only 15% of the time.  This graph is meant to give a quick overview of the flow ranges, variability, and probability of flows of a river segment during the different flow periods of a river; which are High Flows from 0 to 10 percent flow interval, Moist Conditions 10-40, Mid-Range Flows 40-60, Dry Conditions 60-90, and Low Flows 90-100 (Cleland 2003).\n";
            dynamic_paragraph = dynamic_paragraph + "The grey graphed lines are duration curves for each individual year within the analysis period or annual flow duration curves.\n";
        }else{
            dynamic_paragraph = dynamic_paragraph + "Although some observed points may exceed the target curves concentration there is no single apparent pollutant source.\n";
            dynamic_paragraph = dynamic_paragraph + "Please click Further Model Information for more pollutant source identification help.\n";
        }
        //Create references for paragraph
        dynamic_paragraph = dynamic_paragraph + "References:\n";
        dynamic_paragraph = dynamic_paragraph + dataSource + "\n";
        dynamic_paragraph = dynamic_paragraph + "Cleland, B. R. November 2003. TMDL Development from the Bottom Up Part III: Duration Curves and Wet-Weather Assessments. National TMDL Science and Policy 2003.\n"; 
        dynamic_paragraph = dynamic_paragraph + "Cleland, B. R. August 2007. An Approach for Using Load Duration Curves in the Development of TMDLs. National TMDL Science and Policy 2007.";

        return dynamic_paragraph;
    }
    /**
     * 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);
    }
    /**
     * Primary FDC/LDC model
     * It calls the subfunctions based on user selection/inputs.
     * Calls CDWR, STORET, or USGS database queries and their respective subfunctions
     * @throws IOException 
     * @throws InterruptedException 
     */
    public void run() throws IOException, InterruptedException, ParseException, Exception {
    
        //If no date input, make it the maximum of available data
        if(startDate == null || startDate.equalsIgnoreCase("")){
            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);
        }
		
	//Determine which Duration Curve method to run, FDC or LDC
	if(wqTest.equalsIgnoreCase("flow")){
            createFDC();
	}else{
            createLDC();
	}
    }
    public static void main(String[] args) throws IOException, InterruptedException, Exception {
        //Define and run model
        guiDC_Model dc_Model = new guiDC_Model();
        dc_Model.run();
    }
}