guiDC_Model.java [src/java/cfa] Revision: 305ecd4f18aa140914e433d88c0eee24ea51b973  Date: Mon Mar 24 11:32:37 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.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
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: 5-February-2014
* @author Tyler Wible
* @since 12-June-2011
*/
public class guiDC_Model {
    //Inputs
    String mainFolder = "C:/Projects/TylerWible/CodeDirectories/NetBeans/CSIP/data/CFA";
    String modelType = "FDC";//"LDC";//
    String organizationName = "USGS";//"Colorado Dept. of Public Health & Environment";//
    String stationID = "06752280";//"000028";//
    String stationName = "CACHE LA POUDRE RIV AB BOXELDER CRK NR TIMNATH, CO";
    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 = "April";
    String seasonEnd = "October";
    String mQnPeriod = "false";//"7Q10-year";//
    String userData = "";//"Date\tFlow\n1999-04-29\t8.3\n1999-05-09\t60.2\n1999-05-29\t20.1$$Date\tname\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 lowFlowErrorMessage = "";
    double mQnVal = -1;
    
    //Gets
    public File getDurationCurve_summary() {
        return new File(mainFolder, "duration_curve_summary.txt");
    }
    public File getDurationCurve_results() {
        return new File(mainFolder, "duration_curve_results.txt");
    }
    public String getGraph() {
        return "duration_curve_graph.jpg";
    }
    public String getFlowLen() {
        return flowLen;
    }
    public String getWQLen() {
        return wqLen;
    }
    public String getWQUnits() {
        return wqUnits;
    }
    public String getLowFlowErrorMessage(){
        return lowFlowErrorMessage;
    }
    public double getMQNval(){
        return mQnVal;
    }
    public String getStart() {
        return start;
    }
    public String getEnd() {
        return end;
    }
    
    //Sets
    public void setMainFolder(String mainFolder) {
        this.mainFolder = mainFolder;
    }
    public void setModelType(String modelType) {
        this.modelType = modelType;
    }
    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 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;
    }
    /**
     * Performs a Weibull plotting position ranking of the provided flow values in sortedData's second column
     * @param sortedData  a string[][] containing: column1 = dates (unused) column2 = flowValues to be ranked
     * @return  a double[][] containing: column1 = x, column2 = y coordinates of the ranking based on a duration 
     * curve method and Weibull plotting position
     */
    public double[][] durationCurveWeibullRanking(String[][] sortedData){
        //Sort the Data by flow to prepare for ranking
        Arrays.sort(sortedData, new FlowComparator());	

        // Index new variables of rank, and non-exceedence probability
        double total_samples = sortedData.length;
        double[][] xyRanks = new double[sortedData.length + 1][2];
        xyRanks[0][0] = 0;
        int g = 0, h = 0;

        //Flow duration curve using "Weibul" distribution
        for(int i=1; i<(sortedData.length +1); i++){
            if((i != sortedData.length) && (sortedData[i-1][1].equals(sortedData[i][1]))){
                //Find how many elements equal
                h = i;
                while((h != sortedData.length) && (sortedData[h-1][1].equals(sortedData[h][1]))){
                    h++;
                } 
                //Give all the equal elements the rank of the largest equal element (max rank for tied rank scenarios)
                for(g=i; g<=h; g++){
                    xyRanks[g][0] = (h / (total_samples))*100;
                    xyRanks[g-1][1] = Double.parseDouble(sortedData[i-1][1]);
                    if (g==h){
                        //If on the last repeated element, set the initial counter "i" that last 
                        //rank value as to not repeat comparing already sorted and ranked values
                        i=h;
                    }
                }
            }else{
                xyRanks[i-1][0] = (i / (total_samples))*100;
                xyRanks[i-1][1] = Double.parseDouble(sortedData[i-1][1]);
            }
        }
        xyRanks[sortedData.length][0] = 100;

        return xyRanks;
    }
    /**
     * 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 String[] createFDC() throws IOException, InterruptedException{
        //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 low_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, 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, 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(sortableData.length == 0){
                String database = "USGS";
                if(!organizationName.equals("USGS")){
                    database = "STORET";
                }
                errorMessage.add("There is no available flow data in the " + database + " database for station '" + stationID + "' and the specified date range.");
            }
            if(sortableData_user.length == 0){
                errorMessage.add("There is no available uploaded data for station '" + stationID + "' and the specified date range");
            }
            errorMessage.add("Error: Baseflow0001");
            writeError(errorMessage);
        }
        
        //While the dataset is still sorted by date, pull out the mQn value and start/end dates for analysis summary
        int low_m=0, low_n=0;
        String low_period = "";
        if(mQnHide == false){
            //Pull out mQn-period variables
            String mString = mQnPeriod.substring(0,mQnPeriod.indexOf("Q"));
            String nString = mQnPeriod.substring(mQnPeriod.indexOf("Q")+1,mQnPeriod.indexOf("-"));
            low_period = mQnPeriod.substring(mQnPeriod.indexOf("-")+1,mQnPeriod.length());
            low_m = Integer.parseInt(mString);
            low_n = Integer.parseInt(nString);
        }
        mQnFlow mqn = new mQnFlow();
        String[] low_returnValue = mqn.main(low_m,low_n,sortedData_combined,low_period,"minimum");
        String low_MQNmessage = low_returnValue[0];
        double low_mQn = Double.parseDouble(low_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(mainFolder, stationID + " - " + stationName, start, end, sortedData_combined, sortedData_user, low_mQn, low_m, low_n, mQnHide);
        
        //Attach analysis summary to return object
        DoubleMath doubleMath = new DoubleMath();
        if(!mQnHide){
            if(!low_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(low_mQn, 2);
            }
        }

        //Create Display Paragraph
        String[] partial_Paragraph = dynamicParagraph("Flow Duration Curve Overview: ","USGS");

        return partial_Paragraph;
    }
    /**
     * Main graphing function for FDC analysis, plots the provided xyRanks as x,y points on a graph 
     * with or without low flow analysis (low_mQn) added depending if it is non-zero or not
     * @param mainFolder  the output location of the graph
     * @param stationName  the stationID and name of the station for which this timeseries graph is being created (will be incoperated into the graph title)
     * @param startDate the beginning date of analysis (yyyy-mm-dd)
     * @param endDate the ending date of analysis (yyyy-mm-dd)
     * @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 low_mQn  the low flow analysis value, if it is not zero it will be graphed, otherwise it will not be graphed
     * @param low_m  the "m" value of an mQn flow analysis (like 7Q10)
     * @param low_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 mainFolder, 
                        String stationName, 
                        String startDate,
                        String endDate,
                        String[][] sortedData_combined, 
                        String[][] sortedData_user,
                        double low_mQn, 
                        double low_m, 
                        double low_n, 
                        boolean mQnHide) throws IOException{
        //Perform Weibull Plotting Position Ranking for the Flow Duration Curve Method
        double[][] xyRanks = durationCurveWeibullRanking(sortedData_combined);

        //Graph the complete flow duration curve for the time period
        XYSeries FDC_xy = new XYSeries("FDC");
        XYSeries low_mQn_xy = new XYSeries(String.valueOf(low_m) + "Q" + String.valueOf(low_n) + " low flow");
        XYSeries FDC_user = new XYSeries("User Data");
        double y = 0, x = 0;
        int ctr = 0, seriesIndex = 0;
        for(int i=0; i<xyRanks.length; i++){
            y = xyRanks[i][1];
            x = xyRanks[i][0];
            FDC_xy.add(x, y);
            if(i != 0){//get low flow intersection point for mQn flow
                if(xyRanks[i][1] < low_mQn && xyRanks[i-1][1] >= low_mQn){
                    low_mQn_xy.add(x,low_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(low_mQn, 0) != 0){//only show mQn if it is not zero
            XYDataset low_mQn_data = new XYSeriesCollection(low_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,low_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
        DoubleArray doubleArray = new DoubleArray();
        Graphing graphing =  new Graphing();
        String currentYear = startDate.substring(0,4);
        String finalYear = endDate.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 = durationCurveWeibullRanking(partialData);
            graphing.graphSeries(plot, partialRanks, Color.lightGray, seriesIndex);
            seriesIndex++;

            int nextYear = Integer.parseInt(currentYear) + 1;
            if(finalYear.compareToIgnoreCase(String.valueOf(nextYear)) >= 0){
                currentYear = String.valueOf(nextYear);
            }else{
                moreYears = false;
            }
        }
        
        //Set extra graphing preferences
        plot = graphing.setLogAxisPreferences(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 graph_title = "Flow Duration Curve for Station No. " + stationName;
        JFreeChart chart = new JFreeChart(graph_title, graphing.titleFont, plot, showLegend);

        //Write a results file containing the flow duration curve interval (non-exceedence values) and their corresponding discharge values
        writeResults(xyRanks, mainFolder, "Discharge (cfs)");

        //Save resulting graph for proof it works
        try{
            guiDC_Model model = new guiDC_Model();
            String path = mainFolder + File.separator + model.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 String[] createLDC() throws IOException, InterruptedException {
//        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, 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, 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
        String database = "USGS";
        if(!organizationName.equals("USGS")){
            database = "STORET";
        }
        ArrayList<String> errorMessage = new ArrayList<>();
        if(sortedData_combined.length == 0){
            if(sortableData.length == 0){
                errorMessage.add("There is no available flow data in the " + database + " database for station '" + stationID + "' and the specified date range.");
            }
            if(sortableData.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;
        String period = "";
        if(mQnHide == false){
            //Pull out mQn-period variables
            String mString = mQnPeriod.substring(0,mQnPeriod.indexOf("Q"));
            String nString = mQnPeriod.substring(mQnPeriod.indexOf("Q")+1,mQnPeriod.indexOf("-"));
            period = mQnPeriod.substring(mQnPeriod.indexOf("-")+1,mQnPeriod.length());
            m = Integer.parseInt(mString);
            n = Integer.parseInt(nString);
        }
        mQnFlow mqn = new mQnFlow();
        String[] returnValue = mqn.main(m,n,sortedData_combined,period,"minimum");
        String MQNmessage = returnValue[0];
        double mQn = Double.parseDouble(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.getSeasonalWQData(WQdata_combined, seasonBegin, seasonEnd);
        String[][] seasonalWQ_user = doubleArray.getSeasonalWQData(WQdata_user, seasonBegin, seasonEnd);

        //Graph resulting LDC data
        String graphTitle = "Load Duration Curve for Station No. " + stationID + " - " + stationName;
        String yaxisTitle = WQlabel + " [" + endUnits + "]";
        Object[] returnArray = graphLDC(mainFolder, stationName, start, end, graphTitle, yaxisTitle, wqTest, 
        		conversion, wqTarget, 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
        DoubleMath doubleMath = new DoubleMath();
        if(mQnHide){
            this.wqLen = String.valueOf(totalCount);
        }else{
            if(!MQNmessage.equalsIgnoreCase("")){//if the MQNmessage is not blank add it to the dynamic summary
                this.wqLen = String.valueOf(totalCount);
                this.wqUnits = units;
                this.lowFlowErrorMessage = MQNmessage;
            }else{
                this.wqLen = String.valueOf(totalCount);
                this.wqUnits = units;
                this.mQnVal =  doubleMath.round(mQn, 2);
            }
        }

        //Create display paragraph based on the above summary (firstLine) and the provided paragraphTitle for the LDC
        String[] displayParagraph = dynamicParagraph(paragraphTitle, organizationName);

        return displayParagraph;
    }
    /**
     * Sub-graphing function to create combined-range graph
     * @param mainFolder the location where the graph will be saved
     * @param stationName the name of the station to be used in error reporting
     * @param startDate
     * @param endDate
     * @param graph_title  the desired graph title.
     * @param yaxis_title  the desired y-axis title.
     * @param wqTest the test code of the water quailty test being graphed
     * @param conversion
     * @param wqTarget
     * @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 mainFolder,
                            String stationName,
                            String startDate,
                            String endDate,
                            String graph_title, 
                            String yaxis_title,
                            String wqTest,
                            double conversion,
                            double wqTarget,
                            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"
        double[][] xyRanks = durationCurveWeibullRanking(sortedData_combined);


        //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
        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");
        double y = 0, x = 0;
        int ctr = 0;
        for (int i=0; i<xyRanks.length; i++){
            y = xyRanks[i][1] * conversion * wqTarget;
            x = xyRanks[i][0];
            LDC_xy.add(x, 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
        List<Double> high_data = new ArrayList<>();
        List<Double> moist_data = new ArrayList<>();
        List<Double> mid_data = new ArrayList<>();
        List<Double> dry_data = new ArrayList<>();
        List<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 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);


        //Format median series
        XYSeries median_series = new XYSeries("Medians");
        XYDataset median_scatter = new XYSeriesCollection(median_series);
        XYItemRenderer renderer_median = new XYLineAndShapeRenderer(false, true);
        renderer_median.setSeriesShape(0, new Rectangle2D.Double(-4.0, 0.0, 8.0, 0.5));//new Ellipse2D.Double(-4, -4, 8, 8));
        renderer_median.setSeriesPaint(0, Color.red);
        renderer_median.setSeriesVisibleInLegend(0, false);
        plot.setDataset(5, median_scatter);
        plot.setRenderer(5, renderer_median);

        //Create box plot of WQ points
        Graphing graphing = new Graphing();
        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, 8, showOutliers, showExtremeOutliers);
            plot = (XYPlot) returnArray[0];
            showOutliers = (Boolean) returnArray[1];
            showExtremeOutliers = (Boolean) returnArray[2];
        }

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

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

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

        if(low_data.size() > 4){
            //Create quartile rectangle, min-max line, and median line
            Object[] returnArray = graphing.boxplot_shapes(plot, 95, low_data, 31, showOutliers, showExtremeOutliers);
            plot = (XYPlot) returnArray[0];
            showOutliers = (Boolean) returnArray[1];
            showExtremeOutliers = (Boolean) returnArray[2];
        }


        //Graph a LDC for each year in time period
        DoubleArray doubleArray = new DoubleArray();
        int seriesIndex = 32;
        String currentYear = startDate.substring(0,4);
        String finalYear = endDate.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 = durationCurveWeibullRanking(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++;

            int nextYear = Integer.parseInt(currentYear) + 1;
            if(finalYear.compareToIgnoreCase(String.valueOf(nextYear)) >= 0){
                currentYear = String.valueOf(nextYear);
            }else{
                moreYears = false;
            }
        }

        //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.setLogAxisPreferences(plot);

        //Graph plot onto JfreeChart
        JFreeChart chart = new JFreeChart(graph_title, 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 (non-exceedence values) and their corresponding discharge values
        writeResults(xyRanks, mainFolder, yaxis_title);
        //Save resulting graph
        try{
            guiDC_Model model = new guiDC_Model();
            String path = mainFolder + File.separator + model.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;
    }
    /**
     * 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 + "duration_curve_results.txt";
        FileWriter writer =  new FileWriter(path, false);
        PrintWriter printLine = new PrintWriter(writer);

        //Add Headers to text file
        printLine.printf("%s" + "%n", "Flow Duration Interval (Non-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  
     * @param database  Database type to give credit to where the data originated in the reference cited section
     * @return  the dynamic paragraph to be displayed to the user
     */
    public String[] dynamicParagraph(String paragraphTitle, String database) {
        String[] dynamic_paragraph = new String[9];
        
        //Get today's date for the source reference
        DateFormat desiredDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date currentDate = new Date();
        String today = desiredDateFormat.format(currentDate);
        
        String sourceText = "";
        //Determine if current source is USGS or STORET to give credit for the data
        if(database.equals("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{
            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;
        }
        //Create the correct dynamic paragraph
        if(paragraphTitle.equals("Point Sources and Wastewater Sources: ")){
            dynamic_paragraph[1] = "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).";
            dynamic_paragraph[2] = "The grey graphed lines are duration curves for each individual year within the analysis period.";
        }else if(paragraphTitle.equals("Upper Flow Sources: ")){
            dynamic_paragraph[1] = "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).";
            dynamic_paragraph[2] = "The grey graphed lines are duration curves for each individual year within the analysis period.";
        }else if(paragraphTitle.equals("Wet-Weather Sources: ")){
            dynamic_paragraph[1] = "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).";
            dynamic_paragraph[2] = "The grey graphed lines are duration curves for each individual year within the analysis period.";
        }else if(paragraphTitle.equals("Erosion Sources: ")){
            dynamic_paragraph[1] = "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.";
            dynamic_paragraph[2] = "The grey graphed lines are duration curves for each individual year within the analysis period.";
        }else if(paragraphTitle.equals("Multiple Pollution Sources: ")){
            dynamic_paragraph[1] = "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.";
            dynamic_paragraph[2] = "The grey graphed lines are duration curves for each individual year within the analysis period.";
        }else if(paragraphTitle.equals("Flow Duration Curve Overview: ")){
            dynamic_paragraph[1] = "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).";
            dynamic_paragraph[2] = "The grey graphed lines are duration curves for each individual year within the analysis period.";
        }else if(paragraphTitle.equals("Time Series Graph Overview: ")){
            dynamic_paragraph[1] = "A time series graph is a straight scale graphing of available flow data with the oldest date on the bottom left and the most recent date on the bottom right with flows on the y axis.  This can be useful to identify hydrographs from storm runoff for small time frames (ie. less than a couple days worth of data points)";
            dynamic_paragraph[2] = "";
        }else{
            dynamic_paragraph[1] = "Although some observed points may exceed the target curve's concentration there is no single apparent pollutant source.";
            dynamic_paragraph[2] = "Please click 'Further Model Information' for more pollutant source identification help.";
        }
        //Create references for paragraph
        dynamic_paragraph[0] = paragraphTitle;
        dynamic_paragraph[3] = "";
        dynamic_paragraph[4] = "References:";
        dynamic_paragraph[5] =  sourceText;
        dynamic_paragraph[6] = "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."; 
        dynamic_paragraph[7] = "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 dynamically created paragraph to be displayed to the user along with the LDC graph
     * @param dynamicParagraph  string array to be written as each line of the text file
     * @param partialpath  the partial folder path of the file to be written
     * @throws IOException
     */
    public void writeParagraph(String[] dynamicParagraph, String partialpath) throws IOException{
        String path = partialpath + File.separator + "duration_curve_summary.txt";
        FileWriter writer =  new FileWriter(path, false);
        PrintWriter print_line = new PrintWriter(writer);

        //Output data to text file
        for(int i = 0; i < dynamicParagraph.length; i++) {
            print_line.printf("%s" + "%n", dynamicParagraph[i]);
            System.out.println(dynamicParagraph[i]);
        }
        System.out.println("Text File located at:\t" + path);
        print_line.close();
        writer.close();
    }
    /**
     * 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 {
    
        //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
	String[] dynamicParagraph = new String[9];
	if(modelType.equalsIgnoreCase("FDC")){
            dynamicParagraph = createFDC();
	}else if(modelType.equalsIgnoreCase("LDC")){
            dynamicParagraph = createLDC();
	}
        
        //Write dynamic Paragraph to text file
        writeParagraph(dynamicParagraph, mainFolder);
    }
    public static void main(String[] args) throws IOException, InterruptedException, Exception {
        //Define and run model
        guiDC_Model dc_Model = new guiDC_Model();
        dc_Model.run();
    }
}