guiDC_Model.java [src/java/cfa] Revision: 49b4d37216d60e9aa035a3f2f5967705709330f8  Date: Thu Jul 31 09:24:11 MDT 2014
package cfa;

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.Comparator;
import java.util.Date;
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;

class FlowComparator implements Comparator<String[]>{
    //Compares the second entry of sorted data as flow values and sorts largest to smallest
    public int compare(final String[] entry1, final String[] entry2) {
        double value1 = 0;
        double value2 = 0;
        try{
            value1 = Double.parseDouble(entry1[1]);
            value2 = Double.parseDouble(entry2[1]);
        }catch(NumberFormatException e){
            e.printStackTrace();
        }
        if (value1 > value2){
            return -1;
        }else if (value1 < value2){
            return 1;
        }else{
            return 0;
        }
    }
}
/**
* Last Updated: 30-July-2014
* @author Tyler Wible
* @since 12-June-2011
*/
public class guiDC_Model {
    //Inputs
    String mainFolder = "C:/Projects/TylerWible/CodeDirectories/NetBeans/CSIP/data/CFA/DurationCurve";
    String database = "USGS";//"CDWR";//"STORET";//"UserData";//
    String organizationName = "USGS";//"Co. Division of Water Resources";//"Colorado Dept. of Public Health & Environment";//
    String stationID = "06752280";//"CLAGRECO";//"000028";//
    String stationName = "CACHE LA POUDRE RIV AB BOXELDER CRK NR TIMNATH, CO";//"Cache La Poudre Near Greeley";//"BIG THOMPSON R NEAR MOUTH";//
    String modelType = "FDC";//"LDC";//
    String wqTest = "flow";//"00600      Total nitrogen, water, unfiltered, milligrams per liter -- mg/l";//"Nitrogen, Nitrate (NO3) as NO3";//
    double wqTarget = 10;//in units of current test
    String beginDate = "";
    String endDate = "";
    String seasonBegin = "04-01";//"MM-dd"
    String seasonEnd = "09-30";//"MM-dd"
    double highPercentile = 0.75;
    double lowPercentile = 0.25;
    String mQnPeriod = "false";//"7Q10";//
    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";//
    boolean mergeDatasets = false;//true;//
    String mergeMethod = "user";//"public";//"max";//"average";//"min";//
    
    //Outputs
    String flowLen = "-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(mainFolder, "duration_curve_results.txt");
    }
    public File getFlowStatistics_summary() {
        FlowStatistics flowStats = new FlowStatistics();
        return new File(mainFolder, flowStats.getFlowStatistics_summary());
    }
    public String getGraph() {
        return "duration_curve_graph.jpg";
    }
    public File getDCgraphOutput(){
        //This output file is for use with JSHighCharts
        return new File(mainFolder, "duration_curve_graph.out");
    }
    public String getFlowLen() {
        return flowLen;
    }
    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 String getLowFlowErrorMessage(){
        return lowFlowErrorMessage;
    }
    public double getMQNval(){
        return mQnVal;
    }
    
    //Sets
    public void setMainFolder(String mainFolder) {
        this.mainFolder = mainFolder;
    }
    public void setModelType(String modelType) {
        this.modelType = modelType;
    }
    public void setDatabase(String database) {
        this.database = database;
    }
    public void setOrganizationName(String organizationName) {
        this.organizationName = organizationName;
    }
    public void setStationID(String stationID) {
        this.stationID = stationID;
    }
    public void setStationName(String stationName) {
        this.stationName = stationName;
    }
    public void setWaterQualityTest(String wqTest) {
        this.wqTest = wqTest;
    }
    public void setWaterQualityTarget(double wqTarget) {
        this.wqTarget = wqTarget;
    }
    public void setBeginDate(String beginDate) {
        this.beginDate = beginDate;
    }
    public void setEndDate(String endDate) {
        this.endDate = endDate;
    }
    public void setSeasonBegin(String seasonBegin) {
        this.seasonBegin = seasonBegin;
    }
    public void setSeasonEnd(String seasonEnd) {
        this.seasonEnd = seasonEnd;
    }
    public void setHighPercentile(double highPercentile) {
        this.highPercentile = highPercentile;
    }
    public void setLowPercentile(double lowPercentile) {
        this.lowPercentile = lowPercentile;
    }
    public void setMQNperiod(String mQnPeriod) {
        this.mQnPeriod = mQnPeriod;
    }
    public void setUserData(String userData) {
        this.userData = userData;
    }
    public void setMergeDatasets(boolean mergeDatasets) {
        this.mergeDatasets = mergeDatasets;
    }
    public void setMergeMethod(String mergeMethod) {
        this.mergeMethod = mergeMethod;
    }
    /**
     * Main Flow Duration Curve (FDC) function
     * Creates a graph and dynamic summary for the graph, returns an image and text file in 
     * location "mainFolder"/"fileName"graph.jpg and "mainFolder"/"fileName"paragraph.txt
     * @return  returns a String[] containing the dynamically created summary paragraph to accompany the graph created
     * @throws IOException
     * @throws InterruptedException
     */
    private void createFDC() throws IOException, InterruptedException, ParseException, Exception{
        //Inputs
//        String mainFolder = the file location where the resulting load duration curve graph will be saved and the dynamic paragraph text file will be saved
//        String fileName = the name of the graph and text file to be saved (the graph created will be fileNamegraph.txt and the txt file will be fileNameparagraph.txt)
//        String organizationName = the name of the supervising agency of the current station, used to determine if data should be extracted from USGS or STORET
//        String stationID = the station ID for the current station, used to extract data
//        String stationName = the station name for the current station, used to label the graph
//        String beginDate = user specified begin date for analysis (yyyy-mm-dd)
//        String endDate = user specified end date for analysis (yyyy-mm-dd)
//        String mQnPeriod = contains mQn-period or false, if false no low flow mQn analysis will be performed, if not false should contain ex. 7Q10-year for a 7-day, 10-year low flow analysis to be performed
        
        //Check if user wants mQnFlow calculated and displayed
        boolean mQnHide = mQnPeriod.contains("false");	

        //Check if any flow data exists
        Data data = new Data();
        String[][] sortableData = data.extractFlowData(mainFolder, database, organizationName, stationID, beginDate, endDate, userData);
        
        //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){
            User_Data user_Data = new User_Data();
            sortableData_user = user_Data.readUserFile(userData, "flow", beginDate, endDate);
        }
        
        //Sort the Data by date to remove duplicate date entries
        DoubleArray doubleArray = new DoubleArray();
        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<String>();
            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);
        }
        
        //While the dataset is still sorted by date, pull out the mQn value and start/end dates for analysis summary
        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);
        }
        FlowStatistics flowStats = new FlowStatistics();
        Object[] returnValue = flowStats.calculateAllStatisticsSummaries(mainFolder, stationID, stationName, sortedData_combined, highPercentile, lowPercentile, m, n, true, 
                                                     seasonBegin, seasonEnd, "", "", "", "", "", "");
        double mQn = (Double) returnValue[0];
        String MQNmessage = (String) returnValue[1];
        this.start = sortedData_combined[0][0];
        this.end = sortedData_combined[sortedData_combined.length - 1][0];
        this.flowLen = String.valueOf(sortedData_combined.length);
        
        //Create Custom Flow Duration Curve graph
        graphFDC(sortedData_combined, sortedData_user, mQn, m, n, mQnHide);
        
        //Attach analysis summary to return object
        this.summaryParagraph = dynamicParagraph("Flow Duration Curve Overview: ");
        DoubleMath doubleMath = new DoubleMath();
        if(!mQnHide){
            if(!MQNmessage.equalsIgnoreCase("")){//if the MQNmessage is not blank add it to the dynamic summary
                ArrayList<String> errorMessage = new ArrayList<String>();
                errorMessage.add(this.lowFlowErrorMessage);
                writeError(errorMessage);
            }else{
                this.mQnVal =  doubleMath.round(mQn, 2);
            }
        }
    }
    /**
     * 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 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,
                          double mQn, 
                          double m, 
                          double n, 
                          boolean mQnHide) throws IOException{
        //Perform Weibull Plotting Position Ranking for the Flow Duration Curve Method
        DoubleArray doubleArray = new DoubleArray();
        double[][] xyRanks = doubleArray.weibullPlottingPosition(sortedData_combined);

        //Determine summary table from xyRanks for duration curve
        calculatePercentileSummmary(xyRanks, "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");
        String[][] graphData = new String[xyRanks.length][2];
        int ctr = 0, seriesIndex = 0;
        for(int i=0; i<xyRanks.length; i++){
            double x = xyRanks[i][0];
            double y = xyRanks[i][1];
            FDC_xy.add(x, y);
            graphData[i][0] = String.valueOf(x);
            graphData[i][1] = String.valueOf(y);
            if(i != 0){//get low flow intersection point for mQn flow
                if(xyRanks[i][1] < mQn && xyRanks[i-1][1] >= mQn){
                    mQn_xy.add(x,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(x, y);
                    ctr++;
                }
            }
        }

        //Graph resulting FDC and flow value data
        XYPlot plot = new XYPlot();
        boolean showLegend = false;
        //Create X Axis
        ValueAxis xAxis = new NumberAxis("Flow Duration Interval [%]");
        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++;

        //Create low mQn point
        if(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
        Graphing graphing =  new Graphing();
        String currentYear = start.substring(0,4);
        String finalYear = end.substring(0,4);
        boolean moreYears = xyRanks.length > 0;
        while(moreYears){
            //Get current year's data and graph it
            String[][] partialData = doubleArray.getYearsData(sortedData_combined, currentYear);
            double[][] partialRanks = doubleArray.weibullPlottingPosition(partialData);
            graphing.graphSeries(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(mainFolder, 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 (exceedence values) and their corresponding discharge values
        writeResults(xyRanks, mainFolder, "Discharge (cfs)");

        //Save resulting graph for proof it works
        try{
            String path = mainFolder + 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 "mainFolder"/"fileName"graph.jpg and "mainFolder"/"fileName"paragraph.txt
     * @return  returns a String[] containing the dynamically created summary paragraph to accompany the graph created
     * @throws IOException
     * @throws InterruptedException
     */
    private void createLDC() throws IOException, InterruptedException, ParseException {
//        String mainFolder  the file location where the resulting load duration curve graph will be saved and the dynamic paragraph text file will be saved
//        String fileName  the name of the graph and text file to be saved (the graph created will be fileNamegraph.txt and the txt file will be fileNameparagraph.txt)
//        String organizationName  the name of the supervising agency of the current station, used to determine if data should be extracted from USGS or STORET
//        String stationID  the station ID for the current station, used to extract data
//        String stationName  the station name for the current station, used to label the graph
//        String wqTest  the current user selected water quality (wq) test
//        String beginDate  user specified begin date for analysis (yyyy-mm-dd)
//        String endDate  user specified end date for analysis (yyyy-mm-dd)
//        double wqTarget  user specified water quality target for water quality standards for the current wqTest
//        String seasonBegin  user specified begin month for the "season" of analysis  (ex. "April")
//        String seasonEnd  user specified end month of the "season" of analysis  (ex. "August")
//        String mQnPeriod  contains mQn-period or false, if false no low flow mQn analysis will be performed, if not false should contain ex. 7Q10-year for a 7-day, 10-year low flow analysis to be performed


        //Check if any flow and water quality data exists
        Data data = new Data();
        Object[] returnArray1 = data.extractFlow_and_WQdata(mainFolder, database, organizationName, stationID, beginDate, endDate, userData, wqTest);
        String[][] sortableData = (String[][]) returnArray1[0];
        String[][] WQdata = (String[][]) returnArray1[1];
        
        //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){
            User_Data user_Data = new User_Data();
            Object[] returnArray2 = user_Data.readUserFileLDC(userData, wqTest, beginDate, endDate);
            sortableData_user = (String[][]) returnArray2[0];
            WQdata_user = (String[][]) returnArray2[1];
        }
        
        //Sort the Data by date to remove duplicate date entries
        DoubleArray doubleArray = new DoubleArray();
        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<String>();
        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 for station " + stationID+  " by: " + 
                    organizationName + " 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);
        }
        
        //Determine additional information for the model conversions and labels
        int endIndex = wqTest.lastIndexOf(", ");//"00597      Dissolved nitrogen gas, water, unfiltered, milligrams per liter -- mg/l"
        if(endIndex == -1){
            endIndex = wqTest.lastIndexOf("--");
        }
        String WQlabel = wqTest.substring(11,endIndex);//cut off the "98335      " part before the test name and the units after the name

        //Get Units and conversion for current WQ test
        USGS_Data usgs_Data = new USGS_Data();
        String units = usgs_Data.getUSGSwqUnits(wqTest.substring(0,5));//pull just the 5 digit USGS WQ code
        double conversion = usgs_Data.getUSGSwqConversion(units);
        String endUnits = usgs_Data.getUSGSwqEndUnits(units);
		
        //Check if user wants mQnFlow calculated and displayed
        boolean mQnHide = mQnPeriod.contains("false");	
        
        //While the dataset is still sorted by date, pull out the mQn value and start/end dates for analysis summary
        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);
        }
        FlowStatistics flowStats = new FlowStatistics();
        Object[] returnValue = flowStats.calculateAllStatisticsSummaries(mainFolder, stationID, stationName, sortedData_combined, highPercentile, lowPercentile, m, n, true, 
                                            seasonBegin, seasonEnd, "", "", "", "", "", "");
        double 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*conversion*wqTarget;
        this.start = sortedData_combined[0][0];
        this.end = sortedData_combined[sortedData_combined.length - 1][0];
        this.flowLen = String.valueOf(sortedData_combined.length);
        
        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(WQlabel, endUnits, conversion, sortedData_combined, sortedData_user, 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
        this.wqLen = String.valueOf(totalCount);
        this.wqUnits = endUnits;
        this.summaryParagraph = dynamicParagraph(paragraphTitle);
        DoubleMath doubleMath = new DoubleMath();
        if(!mQnHide){
            if(!MQNmessage.equalsIgnoreCase("")){//if the MQNmessage is not blank add it to the dynamic summary
                this.lowFlowErrorMessage = MQNmessage;
            }else{
                this.mQnVal =  doubleMath.round(mQn, 2);
            }
        }
    }
    /**
     * 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 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 don't 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 WQlabel,
                              String endUnits,
                              double conversion,
                              String[][] sortedData_combined,
                              String[][] sortedData_user,
                              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"
        DoubleArray doubleArray = new DoubleArray();
        double[][] xyRanks = doubleArray.weibullPlottingPosition(sortedData_combined);
        
        //Determine summary table from xyRanks for duration curve
        double[][] loadRanks = new double[xyRanks.length][2];
        for(int i=0; i<xyRanks.length; i++){
            loadRanks[i][0] = xyRanks[i][0];
            loadRanks[i][1] = xyRanks[i][1] * conversion * wqTarget;
        }
        calculatePercentileSummmary(loadRanks, "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 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][2];
        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++;
                }
            }
        }
        //Add WQ test data to graph series and generate box plot data
        ArrayList<Double> high_data = new ArrayList<Double>();
        ArrayList<Double> moist_data = new ArrayList<Double>();
        ArrayList<Double> mid_data = new ArrayList<Double>();
        ArrayList<Double> dry_data = new ArrayList<Double>();
        ArrayList<Double> low_data = new ArrayList<Double>();
        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 tempWQValue = Double.parseDouble(WQdata_combined[i][1]);
                    WQ_load = (Double.parseDouble(sortedData_combined[j][1]))*conversion*tempWQValue;
                    WQ_pts.add(xyRanks[j][0],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(xyRanks[j][0],WQ_load);
                            ctr++;
                        }
                    }

                    //Also add to category box plot data
                    if((xyRanks[j][0] <= 10) & (xyRanks[j][0] >= 0)){
                        high_data.add(WQ_load);
                        if(WQ_load > xyRanks[j][1]){
                            highexceed_count++;
                        }
                    }
                    if((xyRanks[j][0] <= 40) & (xyRanks[j][0] > 10)){
                        moist_data.add(WQ_load);
                        if(WQ_load > xyRanks[j][1]){
                            moistexceed_count++;
                        }
                    }
                    if((xyRanks[j][0] <= 60) & (xyRanks[j][0] > 40)){
                        mid_data.add(WQ_load);
                        if(WQ_load > xyRanks[j][1]){
                            midexceed_count++;
                        }
                    }
                    if((xyRanks[j][0] <= 90) & (xyRanks[j][0] > 60)){
                        dry_data.add(WQ_load);
                        if(WQ_load > xyRanks[j][1]){
                            dryexceed_count++;
                        }
                    }
                    if((xyRanks[j][0] <= 100) & (xyRanks[j][0] > 90)){
                        low_data.add(WQ_load);
                        if(WQ_load > xyRanks[j][1]){
                            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(xyRanks[j][0],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(xyRanks[j][0],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 exceedence points and makes them relative to the total count of exceedences.
        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 (wqTest.equalsIgnoreCase("00597") || wqTest.equalsIgnoreCase("00600") || wqTest.equalsIgnoreCase("00601") || wqTest.equalsIgnoreCase("00602") || 
                wqTest.equalsIgnoreCase("00604") || wqTest.equalsIgnoreCase("00605") || wqTest.equalsIgnoreCase("00606") || wqTest.equalsIgnoreCase("00607") || 
                wqTest.equalsIgnoreCase("00608") || wqTest.equalsIgnoreCase("00610") || wqTest.equalsIgnoreCase("00613") || wqTest.equalsIgnoreCase("00615") || 
                wqTest.equalsIgnoreCase("00618") || wqTest.equalsIgnoreCase("00619") || wqTest.equalsIgnoreCase("00620") || wqTest.equalsIgnoreCase("00623") || 
                wqTest.equalsIgnoreCase("00624") || wqTest.equalsIgnoreCase("00625") || wqTest.equalsIgnoreCase("00628") || wqTest.equalsIgnoreCase("00630") || 
                wqTest.equalsIgnoreCase("00631") || wqTest.equalsIgnoreCase("00635") || wqTest.equalsIgnoreCase("00636") || wqTest.equalsIgnoreCase("00639") || 
                wqTest.equalsIgnoreCase("00650") || wqTest.equalsIgnoreCase("00653") || wqTest.equalsIgnoreCase("00660") || wqTest.equalsIgnoreCase("00665") || 
                wqTest.equalsIgnoreCase("00666") || wqTest.equalsIgnoreCase("00667") || wqTest.equalsIgnoreCase("00669") || wqTest.equalsIgnoreCase("00670") || 
                wqTest.equalsIgnoreCase("00671") || wqTest.equalsIgnoreCase("00672") || wqTest.equalsIgnoreCase("00673") || wqTest.equalsIgnoreCase("00674") || 
                wqTest.equalsIgnoreCase("00675") || wqTest.equalsIgnoreCase("00676") || wqTest.equalsIgnoreCase("00677") || wqTest.equalsIgnoreCase("00678") || 
                wqTest.equalsIgnoreCase("01425") || wqTest.equalsIgnoreCase("01465") || wqTest.equalsIgnoreCase("49567") || wqTest.equalsIgnoreCase("49570") || 
                wqTest.equalsIgnoreCase("62854") || wqTest.equalsIgnoreCase("62855") || wqTest.equalsIgnoreCase("64832") || wqTest.equalsIgnoreCase("70507") || 
                wqTest.equalsIgnoreCase("71845") || wqTest.equalsIgnoreCase("71846") || wqTest.equalsIgnoreCase("71850") || wqTest.equalsIgnoreCase("71851") || 
                wqTest.equalsIgnoreCase("71855") || wqTest.equalsIgnoreCase("71856") || wqTest.equalsIgnoreCase("71886") || wqTest.equalsIgnoreCase("71887") || 
                wqTest.equalsIgnoreCase("71888") || wqTest.equalsIgnoreCase("76008") || wqTest.equalsIgnoreCase("76009") || wqTest.equalsIgnoreCase("76010") || 
                wqTest.equalsIgnoreCase("82046") || wqTest.equalsIgnoreCase("83044") || wqTest.equalsIgnoreCase("83047") || wqTest.equalsIgnoreCase("83050") || 
                wqTest.equalsIgnoreCase("83053") || wqTest.equalsIgnoreCase("83056") || wqTest.equalsIgnoreCase("83059") || wqTest.equalsIgnoreCase("83062") ||
                wqTest.equalsIgnoreCase("83065") || wqTest.equalsIgnoreCase("83068") || wqTest.equalsIgnoreCase("83071") || wqTest.equalsIgnoreCase("83074") || 
                wqTest.equalsIgnoreCase("83077") || wqTest.equalsIgnoreCase("83080") || wqTest.equalsIgnoreCase("83083") || wqTest.equalsIgnoreCase("83086") || 
                wqTest.equalsIgnoreCase("83089") || wqTest.equalsIgnoreCase("83092") || wqTest.equalsIgnoreCase("83095") || wqTest.equalsIgnoreCase("83098") || 
                wqTest.equalsIgnoreCase("83101") || wqTest.equalsIgnoreCase("83108") || wqTest.equalsIgnoreCase("83111") || wqTest.equalsIgnoreCase("83114") || 
                wqTest.equalsIgnoreCase("83117") || wqTest.equalsIgnoreCase("83326") || wqTest.equalsIgnoreCase("83329") || wqTest.equalsIgnoreCase("83332") || 
                wqTest.equalsIgnoreCase("83335") || wqTest.equalsIgnoreCase("83338") || wqTest.equalsIgnoreCase("83341") || wqTest.equalsIgnoreCase("83344") || 
                wqTest.equalsIgnoreCase("83347") || wqTest.equalsIgnoreCase("83350") || wqTest.equalsIgnoreCase("83353") || wqTest.equalsIgnoreCase("83356") || 
                wqTest.equalsIgnoreCase("83359") || wqTest.equalsIgnoreCase("83362") || wqTest.equalsIgnoreCase("83365") || wqTest.equalsIgnoreCase("83368") || 
                wqTest.equalsIgnoreCase("83371") || wqTest.equalsIgnoreCase("83374") || wqTest.equalsIgnoreCase("83377") || wqTest.equalsIgnoreCase("83380") || 
                wqTest.equalsIgnoreCase("83383") || wqTest.equalsIgnoreCase("83390") || wqTest.equalsIgnoreCase("83393") || wqTest.equalsIgnoreCase("83396") || 
                wqTest.equalsIgnoreCase("83399") || wqTest.equalsIgnoreCase("90859") || wqTest.equalsIgnoreCase("91003") || wqTest.equalsIgnoreCase("91004") || 
                wqTest.equalsIgnoreCase("99116") || wqTest.equalsIgnoreCase("99120") || wqTest.equalsIgnoreCase("99121") || wqTest.equalsIgnoreCase("99122") || 
                wqTest.equalsIgnoreCase("99123") || wqTest.equalsIgnoreCase("99124") || wqTest.equalsIgnoreCase("99125") || wqTest.equalsIgnoreCase("99126") || 
                wqTest.equalsIgnoreCase("99133") || wqTest.equalsIgnoreCase("99410") || wqTest.equalsIgnoreCase("99411") || wqTest.equalsIgnoreCase("99412") || 
                wqTest.equalsIgnoreCase("99413") || wqTest.equalsIgnoreCase("99414") || wqTest.equalsIgnoreCase("99415") || wqTest.equalsIgnoreCase("99416") || 
                wqTest.equalsIgnoreCase("99889") || wqTest.equalsIgnoreCase("99891") || wqTest.equalsIgnoreCase("99892") || wqTest.equalsIgnoreCase("99893") || 
                wqTest.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);

        //Create X Axis
        ValueAxis xAxis = new NumberAxis("Duration Curve Interval [%]");
        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(0, LDC_data);
        plot.setRenderer(0, renderer1);

        //Put the line on the first Domain and first Range
        plot.mapDatasetToDomainAxis(0, 0);
        plot.mapDatasetToRangeAxis(0, 0);


        //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(1,mQn_data);
            plot.setRenderer(1,renderer2);
            plot.mapDatasetToDomainAxis(1,0);
            plot.mapDatasetToRangeAxis(1,0);
        }

        //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(2,user_data);
            plot.setRenderer(2,renderer);
            plot.mapDatasetToDomainAxis(2,0);
            plot.mapDatasetToRangeAxis(2,0);
        }
        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(4,user_wq_data);
            plot.setRenderer(4,renderer);
            plot.mapDatasetToDomainAxis(4,0);
            plot.mapDatasetToRangeAxis(4,0);
        }
        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(3,user_seasonal_wq_data);
            plot.setRenderer(3,renderer);
            plot.mapDatasetToDomainAxis(3,0);
            plot.mapDatasetToRangeAxis(3,0);
        }

        //Create the scatter data and renderer
        XYDataset scatter_data3 = new XYSeriesCollection(WQ_pts);
        XYDataset scatter_data4 = new XYSeriesCollection(Seasonal_WQ_pts);
        XYItemRenderer renderer3 = new XYLineAndShapeRenderer(false, true);
        XYItemRenderer renderer4 = new XYLineAndShapeRenderer(false, true);
        renderer3.setSeriesShape(0, new Rectangle2D.Double(-3.0, 3.0, 6.0, 6.0));
        renderer4.setSeriesShape(0, new Rectangle2D.Double(-1.5, 4.5, 3.0, 3.0));
        renderer3.setSeriesPaint(0, Color.GREEN);
        renderer4.setSeriesPaint(0, Color.black); 
        //Put the scatter data and renderer into plot
        plot.setDataset(7, scatter_data3);
        plot.setDataset(6, scatter_data4);
        plot.setRenderer(7, renderer3);
        plot.setRenderer(6, renderer4);
        //Put the scatters on the first Domain and first Range
        plot.mapDatasetToDomainAxis(6, 0);
        plot.mapDatasetToDomainAxis(7, 0);
        plot.mapDatasetToRangeAxis(6, 0);
        plot.mapDatasetToRangeAxis(7, 0);

        //Create box plot of WQ points
        Graphing graphing = new Graphing();
        boolean showOutliers = false, showExtremeOutliers = false;
        int seriesIndex = 8;
        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];
        }


        //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 year's 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;
            }
            graphing.graphSeries(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;
            }
        }
        
        //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, partialWQdata_x);//Add WQ x points
        graphData = doubleArray.appendcolumn_Matrix(graphData, partialWQdata_y);//Add WQ y points
        graphData = doubleArray.appendcolumn_Matrix(graphData, partialSeasonalWQdata_x);//Add Seasonal WQ x points
        graphData = doubleArray.appendcolumn_Matrix(graphData, partialSeasonalWQdata_y);//Add Seasonal WQ y points
        
        
        //Output XY data for use with JHighCharts
        doubleArray.writeXYseries(mainFolder, 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 (exceedence values) and their corresponding discharge values
        writeResults(xyRanks, mainFolder, yaxis_title);
        
        //Save resulting graph
        try{
            String path = mainFolder + 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;
    }
    private void calculatePercentileSummmary(double[][] xyRanks, String title, String units){
        DoubleMath doubleMath = new DoubleMath();
        DoubleArray doubleArray = new DoubleArray();

        //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);

        //Round results for summary table
        yFinal = doubleMath.roundColumn(yFinal, 100);

        //Create summary table in a single string
        this.summaryTable = "Exceedence Percentile\t" + title;     //row 1
        summaryTable = summaryTable + "\n(%)\t" + units;  //row 2

        for(int i=0; i<frqDisp.length; i++){
            summaryTable = summaryTable + "\n" + frqDispString[i] + "\t" + String.valueOf(yFinal[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 partialpath  the partial folder path of the file to be written
     * @param resultType  the second column of data header label
     * @throws IOException
     */
    public void writeResults(double[][] results, String partialpath, String resultType) throws IOException{
        //Remove zero results
        ArrayList<String> finalResults = new ArrayList<String>();
        for(int i=0; i<results.length; i++){
            //Only keep non-zero data
            if(Double.compare(results[i][0],0) != 0){
                finalResults.add(results[i][0] + "\t" + results[i][1]);
            }
        }

        String path = partialpath + File.separator + getDurationCurve_results().getName();
        FileWriter writer =  new FileWriter(path, false);
        PrintWriter printLine = new PrintWriter(writer);

        //Add Headers to text file
        printLine.printf("%s" + "%n", "Flow Duration Interval (Excedence Probability based on Weibull Plotting Position Ranking)\t" + resultType);

        //Output data to text file
        for(int i=0; i < finalResults.size(); i++) {
                printLine.printf("%s" + "%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";
        
        //Get today's date for the source reference
        Date currentDate = new Date();
        SimpleDateFormat sourceDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        String today = sourceDateFormat.format(currentDate);
        
        String sourceText = "";
        //Determine if current source is USGS or STORET to give credit for the data
        if(database.equalsIgnoreCase("USGS")){
            sourceText = "Stream flow data and water quality test data courtesy of the U.S. Geological Survey, National Water Information System: Web Interface. http://waterdata.usgs.gov/nwis, accessed: " + today;
        }else if(database.equalsIgnoreCase("STORET")){
            sourceText = "Stream flow data and water quality test data courtesy of the U.S. Environmental Protection Agency, STORET. http://www.epa.gov/storet/index.html accessed: " + today;
        }else if(database.equalsIgnoreCase("CDWR")){
            sourceText = "Stream flow data and water quality test data courtesy of the Colorado Division of Water Resources, CDWR. http://www.dwr.state.co.us accessed: " + today;
        }
        this.dataSource = sourceText;
        
        //Create the correct dynamic paragraph
        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 exceedence.  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.\n";
        }else{
            dynamic_paragraph = dynamic_paragraph + "Although some observed points may exceed the target curve's 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 + sourceText + "\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 LDC model
     * It calls the subfunctions based on user selection/inputs.
     * Calls STORET or USGS database queries and their respective subfunctions
     * @param args  Input variables for the LDC model to run, specifically which model is desired to run, STORET vs. USGS, FDC vs. LDC.
     * @throws IOException 
     * @throws InterruptedException 
     */
    public void run() throws IOException, InterruptedException, ParseException, Exception {
    
        //If no date input, make it the maximum of available data
        if(beginDate == null || beginDate.equalsIgnoreCase("")){
            beginDate = "1900-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);
        }
        
        //If no specified season, create default
        if(seasonBegin == null || seasonBegin.equalsIgnoreCase("") || seasonEnd == null || seasonEnd.equalsIgnoreCase("")){
            seasonBegin = "April";
            seasonEnd = "October";
        }	
		
	//Determine which Duration Curve method to run, FDC or LDC
	if(modelType.equalsIgnoreCase("FDC")){
            createFDC();
	}else if(modelType.equalsIgnoreCase("LDC")){
            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();
    }
}