DurationCurve.java [src/java/cfa] Revision: ed4e4a960882eb4a0da28bca0df90ba790549b15  Date: Fri Oct 18 10:18:06 MDT 2013
package cfa;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Stroke;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
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.IntervalMarker;
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;
import org.jfree.ui.Layer;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.TextAnchor;

class DateComparator implements Comparator<String[]>{
    //Compares the first entry of sorted data as flow values and sorts largest to smallest
    public int compare(final String[] entry1, final String[] entry2) {
        String value1 = entry1[0];
        String value2 = entry2[0];
        int comparison = value1.compareTo(value2);
        if(comparison>0){
            return 1;
        }else if(comparison<0){
            return -1;
        }else{
            return 0;
        }
    }
}
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: 8-August-2013
* @author Tyler Wible
* @since 21-June-2011
*/
public class DurationCurve {
    private String fontFamily = "SansSerif";
    Font titleFont = new Font(fontFamily, Font.BOLD, 30);
    Font masterFont = new Font(fontFamily, Font.PLAIN, 22);
    Font DClabelFont = new Font(fontFamily, Font.ITALIC, 20);
    Font floodLabelFont = new Font(fontFamily, Font.PLAIN, 18);
    //Calibri
    //Georgia
    //SansSerif
    //Serif
    //Tahoma
    //Verdana
    /**
     * Sub-graphing function to add graph x-interval marker/label
     * @param Label_title  the desired label title.
     * @param lowerlimit  the lower limit of the x-interval marker.
     * @param upperlimit  the upper limit of the x-interval marker.
     * @return An Interval Marker.
     */
    public XYPlot addIntervalLabel (XYPlot plot, String Label_title, double lowerlimit, double upperlimit){
        //Create a new interval marker and set its properties
        final IntervalMarker newLabel = new IntervalMarker(lowerlimit,upperlimit);
        newLabel.setLabel(Label_title);
        newLabel.setLabelFont(DClabelFont);
        newLabel.setLabelAnchor(RectangleAnchor.BOTTOM);
        newLabel.setLabelTextAnchor(TextAnchor.BASELINE_CENTER);
        newLabel.setPaint(plot.getBackgroundPaint());//new Color(250, 250, 250, 100));//new Color(222, 222, 255, 130));
        newLabel.setOutlinePaint(Color.black);

        //Add the interval to the provided plot
        plot.addDomainMarker(newLabel, Layer.BACKGROUND);
        
        return plot;
    }
    /**
     * Sub-function to create lines/rectangle to mimic a box plot
     * @param plot  the plot in which the objects are being graphed
     * @param x_coord  x coordinate of the correct boxplot
     * @param data  water quality data within the current flow interval
     * @param series_int  which element in the plot series is being created
     * @param existingOutliers  a variable indicating if there are already 
     * existing outliers (true) which are already labeled in the legend, 
     * therefore don't label these outliers in the legend
     * @param existingExtremeOutliers  a variable indicating if there are already 
     * existing extreme outliers (true) which are already labeled in the legend, 
     * therefore don't label these outliers in the legend
     * @return the original plot with new object in it.
     */
    public Object[] boxplot_shapes(XYPlot plot, double x_coord, 
    		                     List<Double> data, 
    		                     int series_int, 
    		                     boolean existingOutliers, 
    		                     boolean existingExtremeOutliers){
        DoubleMath doubleMath = new DoubleMath();

        //Calculate and add Median to dataset
        XYSeries median_series = new XYSeries("Medians");
        double mid_median = doubleMath.Percentile_function(data,0.50);
        median_series.add(x_coord,mid_median);

        //Create median Line
        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(series_int, median_scatter);
        plot.setRenderer(series_int, renderer_median);


        //Create quartile Box shapes for the box plot
        //Create XYSeries for the box shape
        XYSeries shapeSeries = new XYSeries("Shape");
        double lowerQuartile = doubleMath.Percentile_function(data,0.25);
        double upperQuartile = doubleMath.Percentile_function(data,0.75);
        shapeSeries.add(x_coord, lowerQuartile);
        shapeSeries.add(x_coord, upperQuartile);

        //Create the quartile rectangle shape
        XYDataset shapeDataset = new XYSeriesCollection(shapeSeries);
        XYItemRenderer renderer_shape = new XYLineAndShapeRenderer(true, false);
        Stroke thickness = new BasicStroke(8);
        renderer_shape.setSeriesStroke(0, thickness);
        renderer_shape.setSeriesPaint(0, Color.blue);
        renderer_shape.setSeriesVisibleInLegend(0, false);
        plot.setDataset(series_int + 1, shapeDataset);
        plot.setRenderer(series_int + 1, renderer_shape);


        //Creates 1.5 * Interquartile Range (IQR) lines
        //Create XYSeries for the min-max lines
        double IQR = upperQuartile - lowerQuartile;
        double lowerLimit = lowerQuartile - 1.5*IQR;
        double upperLimit = upperQuartile + 1.5*IQR;
        if(lowerLimit < doubleMath.Min_Max(data,false)){
            lowerLimit = doubleMath.Min_Max(data,false);
        }
        if(upperLimit > doubleMath.Min_Max(data,true)){
            upperLimit = doubleMath.Min_Max(data,true);
        }
        XYSeries lineSeries = new XYSeries("Line");
        lineSeries.add(x_coord, lowerLimit);
        lineSeries.add(x_coord, upperLimit);

        //Create the 1.5*IQR lines
        XYDataset lineDataset = new XYSeriesCollection(lineSeries);
        XYItemRenderer lineRenderer = new XYLineAndShapeRenderer(true, true);
        Stroke thickness2 = new BasicStroke(1);
        lineRenderer.setSeriesStroke(0, thickness2);
        lineRenderer.setSeriesShape(0, new Rectangle2D.Double(-10.0, 0.0, 20.0, 1));
        lineRenderer.setSeriesPaint(0, Color.black);
        lineRenderer.setSeriesVisibleInLegend(0, false);
        plot.setDataset(series_int + 2, lineDataset);
        plot.setRenderer(series_int + 2, lineRenderer);

        //Calculate and create Outliers (# < lowerQuartile - 1.5*IQR or # > upperQuartile + 1.5*IQR)
        //Calculate and create Extreme Outliers (# < lowerQuartile - 3*IQR or # > upperQuartile + 3*IQR)
        XYSeries outliers = new XYSeries("Outliers");
        XYSeries extremeOutliers = new XYSeries("Extreme Outliers");
        for(int i=0; i<data.size(); i++){
            double value = data.get(i);
            //Lower outliers
            if(value < (lowerQuartile - 1.5*IQR) && value > (lowerQuartile - 3*IQR)){
                outliers.add(x_coord, value);
            }
            //Upper outliers
            if(value > (upperQuartile + 1.5*IQR) && value < (lowerQuartile + 3*IQR)){
                outliers.add(x_coord, value);
            }

            //Extreme Lower outliers
            if(value < (lowerQuartile - 3*IQR)){
                extremeOutliers.add(x_coord, value);
            }
            //Extreme Upper outliers
            if(value > (lowerQuartile + 3*IQR)){
                extremeOutliers.add(x_coord, value);
            }
        }

        //Create outlier scatter
        XYDataset outlier_scatter = new XYSeriesCollection(outliers);
        XYItemRenderer renderer_outlier = new XYLineAndShapeRenderer(false, true);
        renderer_outlier.setSeriesShape(0, new Ellipse2D.Double(-2.0, 2.0, 4.0, 4.0));
        renderer_outlier.setSeriesPaint(0, Color.darkGray);
        if(outliers.isEmpty() || existingOutliers == true){
            renderer_outlier.setSeriesVisibleInLegend(0, false);        	
        }else{
            existingOutliers = true;
        }
        plot.setDataset(series_int + 3, outlier_scatter);
        plot.setRenderer(series_int + 3, renderer_outlier);

        //Create extreme outlier scatter
        XYDataset extremeOutlier_scatter = new XYSeriesCollection(extremeOutliers);
        XYItemRenderer renderer_ExtremeOutlier = new XYLineAndShapeRenderer(false, true);
        renderer_ExtremeOutlier.setSeriesShape(0, new Ellipse2D.Double(-2.0, 2.0, 4.0, 4.0));
        renderer_ExtremeOutlier.setSeriesPaint(0, Color.red);
        if(extremeOutliers.isEmpty() || existingExtremeOutliers == true){
            renderer_ExtremeOutlier.setSeriesVisibleInLegend(0, false);        	
        }else{
            existingExtremeOutliers = true;
        }
        plot.setDataset(series_int + 4, extremeOutlier_scatter);
        plot.setRenderer(series_int + 4, renderer_ExtremeOutlier);
        
        Object[] returnArray = {plot, existingOutliers, existingExtremeOutliers};
        return returnArray;
    }
    /**
     * Checks if the provided dates are subsequent dates, aka nextDate = date + 1day.  
     * This check includes December 31st to January 1st catchs, 4-year leap-year catches, 
     * 100-year non-leap-year catches and 400-year leap-year catches
     * @param date  the first date to be compared (expected format = yyyy-mm-dd)
     * @param nextDate  the second date to be compared (expected format = (yyyy-mm-dd)
     * @return returns true if nextDate = date + 1day, false otherwise
     */
    public boolean checkSubsequentDates(String date, String nextDate){
        double year = Double.parseDouble(date.substring(0,4));
        double month = Double.parseDouble(date.substring(5,7));
        double day = Double.parseDouble(date.substring(8));

        double year2 = Double.parseDouble(nextDate.substring(0,4));
        double month2 = Double.parseDouble(nextDate.substring(5,7));
        double day2 = Double.parseDouble(nextDate.substring(8));

        boolean subsequentDates = false;
        //Check if nextDate = date + 1 day

        if(Double.compare(year,year2) == 0){//Check if same year
            if(Double.compare(month, month2) == 0){//Check if same month
                if(Double.compare(day + 1, day2) == 0){//Check if subsequent day
                    subsequentDates = true;	
                }
            }else{
                if((Double.compare(month + 1, month2) == 0) && //Check if subsequent month
                        (Double.compare(day2,1) == 0)){//Check if first day
                    //Check months with 31 days
                    if((Double.compare(day, 31) == 0) &&
                            ((Double.compare(month, 1) == 0) ||
                            (Double.compare(month, 3) == 0) ||
                            (Double.compare(month, 5) == 0) ||
                            (Double.compare(month, 7) == 0) ||
                            (Double.compare(month, 8) == 0) ||
                            (Double.compare(month, 10) == 0))){
                        subsequentDates = true;

                    //Check months with 30 days
                    }else if((Double.compare(day, 30) == 0) &&
                            ((Double.compare(month, 4) == 0) ||
                            (Double.compare(month, 6) == 0) ||
                            (Double.compare(month, 9) == 0) ||
                            (Double.compare(month, 11) == 0))){
                        subsequentDates = true;
                    
                    //Check February for leap years (including the 100-year-not-leap-year and 400-year-leap-year)
                    }else if((Double.compare(month, 2) == 0)){
                        boolean leapYear = false;
                        double yearUp4 = Math.ceil(year/4);
                        double yearDown4 = Math.floor(year/4);
                        double yearUp100 = Math.ceil(year/100);
                        double yearDown100 = Math.floor(year/100);
                        double yearUp400 = Math.ceil(year/400);
                        double yearDown400 = Math.floor(year/400);
                        if(yearUp400 == yearDown400){
                            leapYear = true;
                        }else if(yearUp100 == yearDown100){
                            leapYear = false;
                        }else if(yearUp4 == yearDown4){
                            leapYear = true;
                        }
                        //Check non-leap years (28 day February)
                        if(!leapYear  && (Double.compare(day, 28) == 0)){//Check if is subsequent day
                            subsequentDates = true;

                        //Check leap years (29 day February)
                        }else if(leapYear && (Double.compare(day, 29) == 0)){//Check if is subsequent day
                            subsequentDates = true;
                        }
                    }
                }
            }

        }else{
            //Check if subsequent years, months, and days from December 31st to January 1st
            if((Double.compare(year + 1, year2) == 0) && //Check if subsequent years
                    (Double.compare(month, 12) == 0) && //and the first date is December
                    (Double.compare(month2, 1) == 0) && //and the second date is January
                    (Double.compare(day, 31) == 0) && //and the first date is the 31st
                    (Double.compare(day2, 1) == 0)){ //and the second date is the 1st
                subsequentDates = true;
            }
        }

        return subsequentDates;
    }
    /**
     * Sub-function to convert month text to month integer
     * @param month_String  the text version of the month (Ex. January).
     * @return An Integer (Ex. 1).
     */
     public int convertMonth (String month_String){
        int month_number = 0;
        if (month_String.equalsIgnoreCase("January")){
            month_number = 1;
        }else if (month_String.equalsIgnoreCase("February")){
            month_number = 2;
        }else if (month_String.equalsIgnoreCase("March")){
            month_number = 3;
        }else if (month_String.equalsIgnoreCase("April")){
            month_number = 4;
        }else if (month_String.equalsIgnoreCase("May")){
            month_number = 5;
        }else if (month_String.equalsIgnoreCase("June")){
            month_number = 6;
        }else if (month_String.equalsIgnoreCase("July")){
            month_number = 7;
        }else if (month_String.equalsIgnoreCase("August")){
            month_number = 8;
        }else if (month_String.equalsIgnoreCase("September")){
            month_number = 9;
        }else if (month_String.equalsIgnoreCase("October")){
            month_number = 10;
        }else if (month_String.equalsIgnoreCase("November")){
            month_number = 11;
        }else if (month_String.equalsIgnoreCase("December")){
            month_number = 12;
        }
        return month_number;
     }
    /**
     * 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[] sorted_rank = new double[sortedData.length];
        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++){
                    sorted_rank[g-1] = h;
                    xyRanks[g][0] = (sorted_rank[g-1] / (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{
                sorted_rank[i-1] = i;
                xyRanks[i-1][0] = (sorted_rank[i-1] / (total_samples))*100;
                xyRanks[i-1][1] = Double.parseDouble(sortedData[i-1][1]);
            }
        }

        //duplicate last value to match output of matlab's "ecdf" function
        if(sortedData.length >= 2){
            double third_last_entry = Double.parseDouble(sortedData[sortedData.length - 2][1]);
            double second_last_entry = Double.parseDouble(sortedData[sortedData.length - 1][1]);
            double last_entry = 0;
            if((second_last_entry == 0) && (third_last_entry != 0)){
                second_last_entry = 0.0001;
            }else{
                last_entry = second_last_entry - 0.01;
                if(last_entry < 0){
                    last_entry = 0;
                }
            }
            xyRanks[sortedData.length - 1][1] = second_last_entry;
            xyRanks[sortedData.length][1] = last_entry;
        }

        return xyRanks;
    }
    /**
     * 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;
    }
    /**
     * Reduces the provided data to only within the specified year
     * @param allData  all the data for the to be minimized by year (column1 = date (yyyy-mm-dd), column2 = value)
     * @param year  the specified year (ex "1981")
     * @return an array (same format as allData) containing the dates and values that occur within the specified year
     */
    public String[][] getAnnualData(String[][] allData, String year){
        //Remove data that are outside specified year
        int ctr = 0;
        for(int i=0; i<allData.length; i++){
            String tempYear = allData[i][0].substring(0,4);
            if(year.compareToIgnoreCase(tempYear) == 0){
                ctr++;
            }
        }

        String[][] SeasonalData = new String[ctr][2];
        ctr = 0;
        for(int i=0; i<allData.length; i++){
            String tempYear = allData[i][0].substring(0,4);
            if(year.compareToIgnoreCase(tempYear) == 0){
                SeasonalData[ctr][0] = allData[i][0];
                SeasonalData[ctr][1] = allData[i][1];
                ctr++;
            }
        }

        return SeasonalData;
    }
    /**
     * Reduces the provided water quality data to only within the user specified season
     * @param allWQdata  all the WQdata for the user specified date range to be minimized by season (column1 = date (yyyy-mm-dd), column2 = value)
     * @param seasonBegin  the user specified begin month of the season (ex "February")
     * @param seasonEnd  the user specified end month of the season (ex "April")
     * @return  an array containing the dates and values of water quality tests that occur within the specified season
     */
    public String[][] getSeasonalWQData(String[][] allWQdata, String seasonBegin, String seasonEnd){
        //Convert season month into season integer
        int seasonBegin_number = convertMonth(seasonBegin);
        int seasonEnd_number = convertMonth(seasonEnd);

        //Remove water quality tests that are outside user specified seasonal range
        int ctr = 0;
        if(seasonBegin_number < seasonEnd_number){
            for(int i=0; i<allWQdata.length; i++){
                int tempdate = Integer.parseInt(allWQdata[i][0].substring(5,7));
                if((tempdate > seasonBegin_number) && (tempdate < seasonEnd_number)){
                    ctr++;
                }
            }
        }else{
            for(int i=0; i<allWQdata.length; i++){
                int tempdate = Integer.parseInt(allWQdata[i][0].substring(5,7));
                if((tempdate > seasonBegin_number) || (tempdate < seasonEnd_number)){
                    ctr++;
                }
            }
        }
        String[][] SeasonalWQdata = new String[ctr][2];
        ctr = 0;
        if(seasonBegin_number < seasonEnd_number){
            for(int i=0; i<allWQdata.length; i++){
                int tempdate = Integer.parseInt(allWQdata[i][0].substring(5,7));
                if((tempdate > seasonBegin_number) && (tempdate < seasonEnd_number)){
                    SeasonalWQdata[ctr][0] = allWQdata[i][0];
                    SeasonalWQdata[ctr][1] = allWQdata[i][1];
                    ctr++;
                }
            }
        }else{
            for(int i=0; i<allWQdata.length; i++){
                int tempdate = Integer.parseInt(allWQdata[i][0].substring(5,7));
                if((tempdate > seasonBegin_number) || (tempdate < seasonEnd_number)){
                    SeasonalWQdata[ctr][0] = allWQdata[i][0];
                    SeasonalWQdata[ctr][1] = allWQdata[i][1];
                    ctr++;
                }
            }
        }

        return SeasonalWQdata;
    }
    /**
     * 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 startDatd 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 
     */
    public 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
        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 = getAnnualData(sortedData_combined, currentYear);
            double[][] partialRanks = durationCurveWeibullRanking(partialData);
            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 plot preferences
        plot.setOutlinePaint(Color.black);
        plot.setDomainGridlinePaint(Color.black);
        plot.setRangeGridlinePaint(Color.black);
        setAxisFonts(plot);

        //Add Flow Range Labels:
        plot = addIntervalLabel(plot, "High Flow", 0, 10);
        plot = addIntervalLabel(plot, "Moist Conditions", 10, 40);
        plot = addIntervalLabel(plot, "Mid-Range Flows", 40, 60);
        plot = addIntervalLabel(plot, "Dry Conditions", 60, 90);
        plot = 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, 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());
        }
    }
    /**
     * 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 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 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 
     */
    public 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];
            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<Double>();
        List<Double> moist_data = new ArrayList<Double>();
        List<Double> mid_data = new ArrayList<Double>();
        List<Double> dry_data = new ArrayList<Double>();
        List<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/wqTarget;
                    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/wqTarget;
                            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/wqTarget;
                            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
        boolean showOutliers = false, showExtremeOutliers = false;
        if(high_data.size() > 1){
            //Create quartile rectangle, min-max line, and median line
            Object[] returnArray = 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() > 1){
            //Create quartile rectangle, min-max line, and median line
            Object[] returnArray = 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() > 1){
            //Create quartile rectangle, min-max line, and median line
        	Object[] returnArray = 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() > 1){
            //Create quartile rectangle, min-max line, and median line
        	Object[] returnArray = 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() > 1){
            //Create quartile rectangle, min-max line, and median line
        	Object[] returnArray = boxplot_shapes(plot, 95, low_data, 31, showOutliers, showExtremeOutliers);
            plot = (XYPlot) returnArray[0];
            showOutliers = (Boolean) returnArray[1];
            showExtremeOutliers = (Boolean) returnArray[2];
        }


        //Graph a FDC for each year in time period
        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 = getAnnualData(sortedData_combined, currentYear);
            double[][] partialRanks = durationCurveWeibullRanking(partialData);
            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 = addIntervalLabel(plot, "High Flow", 0, 10);
        plot = addIntervalLabel(plot, "Moist Conditions", 10, 40);
        plot = addIntervalLabel(plot, "Mid-Range Flows", 40, 60);
        plot = addIntervalLabel(plot, "Dry Conditions", 60, 90);
        plot = addIntervalLabel(plot, "Low Flow", 90, 100);

        //Set extra plot preferences
        plot.setOutlinePaint(Color.black);
        plot.setDomainGridlinePaint(Color.black);
        plot.setRangeGridlinePaint(Color.black);
        setAxisFonts(plot);


        //Graph plot onto JfreeChart
        JFreeChart chart = new JFreeChart(graph_title, titleFont, plot, true);


        //Set legend Font
        LegendTitle legendTitle = chart.getLegend();
        legendTitle.setItemFont(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;
    }
    /**
     * Graphs the provided "series" as an XY series with x as the first column y as the second column in the provided color
     * on the provided XYPlot, this series is not visible in the legend
     * @param plot  the XYPlot to add the "series" to
     * @param series  a double[][] array with series[all][0] =  x-values, series[all][1] = y-values
     * @param lineColor  the color of the line to be graphed
     * @param seriesIndex  the graph index of the series to be plotted
     * @return the provided XYPlot with the series added to it with the above properties
     */
    public XYPlot graphSeries(XYPlot plot, double[][] series, Color lineColor, int seriesIndex){
        XYSeries xySeries = new XYSeries("");
        for(int i=0; i<series.length; i++){
            double y = series[i][1];
            double x = series[i][0];
            xySeries.add(x, y);
        }

        //Create a graph with the line
        XYDataset xyDataset = new XYSeriesCollection(xySeries);
        XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false);
        renderer.setSeriesPaint(0, lineColor);
        renderer.setSeriesVisibleInLegend(0, false);

        //Set the FDC line data, renderer, and axis into plot
        plot.setDataset(seriesIndex, xyDataset);
        plot.setRenderer(seriesIndex, renderer);

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

        return plot;
    }
    /**
     * Sorts the String[][] by the first column (dates) and then removes duplicate 
     * date values and returns an array of equal or lesser size than the original
     * @param currentStringArray the String[][] to be sorted and removed, column1 = dates (yyyy-mm-dd), column2 = values, other columns will also be kept
     * @return  a String[][] of equal or lesser size than the original containing the original data sorted by the first column
     */
    public String[][] removeDuplicateDates(String[][] currentStringArray){
        //Sort the Data by date to remove duplicate date entries
        Arrays.sort(currentStringArray, new DateComparator());
        
        //Check and remove duplicate days as to correct statistical implication of duplicate flow values on a day
        int ctr=0;
        for(int i=0; i<(currentStringArray.length); i++){
            if(i == 0){
                ctr++;
                continue;
            }
            if(!currentStringArray[i-1][0].equals(currentStringArray[i][0])){
                ctr++;
            }
        }
        String[][] sortedData = new String[ctr][2];
        ctr=0;
        for(int i=0; i<(currentStringArray.length); i++){
            if(i==0){
                sortedData[ctr][0] = currentStringArray[i][0];
                sortedData[ctr][1] = currentStringArray[i][1];
                ctr++;
                continue;
            }
            if(!currentStringArray[i-1][0].equals(currentStringArray[i][0])){
                sortedData[ctr][0] = currentStringArray[i][0];
                sortedData[ctr][1] = currentStringArray[i][1];
                ctr++;
            }
        }

        return sortedData;
    }
    /**
     * This subfunction takes the provided plot and changes the fonts of the axis to a standardized new font
     * @param plot  the XYPlot containing the graph on which the fonts will be changed
     * @return the original plot with the modifications to the axis fonts
     */
    private XYPlot setAxisFonts(XYPlot plot){
        //Set Y axis fonts
        LogarithmicAxis yAxis = (LogarithmicAxis) plot.getRangeAxis();
        yAxis.setLabelFont(masterFont);
        yAxis.setTickLabelFont(masterFont);

        //Set X axis fonts
        ValueAxis xAxis = (ValueAxis) plot.getDomainAxis();
        xAxis.setLabelFont(masterFont);
        xAxis.setTickLabelFont(masterFont);	

        return plot;
    }
    /**
     * @param parameterCode  USGS parameter code for a specific water quality test.
     * @return a string with the type of units for the current test.
     */
     public String USGSwqUnits(String parameterCode) throws IOException{
         URL webpage = new URL("http://nwis.waterdata.usgs.gov/usa/nwis/pmcodes?radio_pm_search=param_group&pm_group=All+--+include+all+parameter+groups&pm_search=&casrn_search=&srsname_search=&format=rdb&show=parameter_group_nm&show=parameter_nm&show=casrn&show=srsname&show=parameter_units");
         URLConnection yc = webpage.openConnection();
         BufferedReader in = new BufferedReader(new InputStreamReader(yc.getInputStream()));
         String inputLine;
         int line_length = 0;
         String units = "0";
         //Find the units of the specified test
         while ((inputLine = in.readLine()) != null) {
             String[] f = inputLine.split("\t");
             line_length = f.length;
             if((line_length >= 6) && (f[0].length() == 5)){
                 if(f[0].equals(parameterCode)){
                     units = f[5];
                 }
             }
         }
         return units;
     }
     /**
     * @param units  the units of the current USGS water quality test.
     * @return a double with the correct conversion factor for the units.
     */
     public double USGSwqConversion(String units){
         double conversion = 0;
         if(units.equalsIgnoreCase("#/l")){
             conversion = (1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("#/m3")){
             conversion = (java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("#/ml")){
             conversion = (1000)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("MPN/100 ml")){
             conversion = (100)*(1000)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("MPN/100L")){
             conversion = (100)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("cfu/100ml")){
             conversion = (100)*(1000)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("cfu/mL")){
             conversion = (1000)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("col/mL")){
             conversion = (1000)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("cysts/100L")){
             conversion = (100)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("cysts/10L")){
             conversion = (10)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("g/cm3") || units.equalsIgnoreCase("g/mL @ 20C")){
             conversion = (java.lang.Math.pow(10,-6))*(1/1)*(1000)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("g/m3")){
             conversion = (java.lang.Math.pow(10,-6))*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("mg/l") || units.equalsIgnoreCase("mg/l CaCO3") || units.equalsIgnoreCase("mg/l NH4") || 
                     units.equalsIgnoreCase("mg/l NO3") || units.equalsIgnoreCase("mg/l PO4") || units.equalsIgnoreCase("mg/l SiO2") || 
                     units.equalsIgnoreCase("mg/l as H") || units.equalsIgnoreCase("mg/l as N") || units.equalsIgnoreCase("mg/l as Na") || 
                     units.equalsIgnoreCase("mg/l as P") || units.equalsIgnoreCase("mg/l as S") || units.equalsIgnoreCase("mgC3H6O2/L")){
             conversion = (java.lang.Math.pow(10,-6))*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("mg/mL @25C")){
             conversion = (java.lang.Math.pow(10,-6))*(1000)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("ml/l")){
             conversion = (1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("ng/l") || units.equalsIgnoreCase("pg/mL")){
             conversion = (java.lang.Math.pow(10,-12))*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("ng/m3") || units.equalsIgnoreCase("pg/l")){
             conversion = (java.lang.Math.pow(10,-12))*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("ocyst/100L")){
             conversion = (100)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("oocyst/10L")){
             conversion = (10)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("pfu/100L")){
             conversion = (100)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("pfu/100ml")){
             conversion = (100)*(1000)*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("pg/m3")){
             conversion = (java.lang.Math.pow(10,-15))*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("ug/L 2,4-D") || units.equalsIgnoreCase("ug/L U3O8") || units.equalsIgnoreCase("ug/L as As") || 
                     units.equalsIgnoreCase("ug/L as Cl") || units.equalsIgnoreCase("ug/L as N") || units.equalsIgnoreCase("ug/L as P") || 
                     units.equalsIgnoreCase("ug/l") || units.equalsIgnoreCase("ugAtrazn/L")){
             conversion = (java.lang.Math.pow(10,-9))*(1000)*(java.lang.Math.pow(0.3048,3))*(86400);
         }else if(units.equalsIgnoreCase("ug/m3")){
             conversion = (java.lang.Math.pow(10,-9))*(java.lang.Math.pow(0.3048,3))*(86400);
         }
         return conversion;
     }
     /**
     * @param units  the units of the current USGS water quality test.
     * @return a string with the end result units of the conversion.
     */
     public String USGSwqEndUnits(String units){
         String endUnits = "No Units";
         if(units.equalsIgnoreCase("#/l") || units.equalsIgnoreCase("#/m3") || units.equalsIgnoreCase("#/ml")){
             endUnits = "#/day";
         }else if(units.equalsIgnoreCase("MPN/100 ml") || units.equalsIgnoreCase("MPN/100L")){
             endUnits = "MPN/day";
         }else if(units.equalsIgnoreCase("cfu/100ml") || units.equalsIgnoreCase("cfu/mL")){
             endUnits = "cfu/day";
         }else if(units.equalsIgnoreCase("col/mL")){
             endUnits = "col/day";
         }else if(units.equalsIgnoreCase("cysts/100L") || units.equalsIgnoreCase("cysts/10L")){
             endUnits = "cysts/day";//= cysts/100L*cfs
         }else if(units.equalsIgnoreCase("mg/l") || units.equalsIgnoreCase("mg/l CaCO3") || units.equalsIgnoreCase("mg/l NH4") || 
                 units.equalsIgnoreCase("mg/l NO3") || units.equalsIgnoreCase("mg/l PO4") || units.equalsIgnoreCase("mg/l SiO2") || 
                 units.equalsIgnoreCase("mg/l as H") || units.equalsIgnoreCase("mg/l as N") || units.equalsIgnoreCase("mg/l as Na") || 
                 units.equalsIgnoreCase("mg/l as P") || units.equalsIgnoreCase("mg/l as S") || units.equalsIgnoreCase("mgC3H6O2/L") || 
                 units.equalsIgnoreCase("g/cm3") || units.equalsIgnoreCase("g/mL @ 20C") || units.equalsIgnoreCase("g/m3") || 
                 units.equalsIgnoreCase("mg/mL @25C") || units.equalsIgnoreCase("ng/l") || units.equalsIgnoreCase("pg/mL") ||
                 units.equalsIgnoreCase("ng/m3") || units.equalsIgnoreCase("pg/l") || units.equalsIgnoreCase("pg/m3") || 
                 units.equalsIgnoreCase("ug/L 2,4-D") || units.equalsIgnoreCase("ug/L U3O8") || units.equalsIgnoreCase("ug/L as As") || 
                 units.equalsIgnoreCase("ug/L as Cl") || units.equalsIgnoreCase("ug/L as N") || units.equalsIgnoreCase("ug/L as P") || 
                 units.equalsIgnoreCase("ug/l") || units.equalsIgnoreCase("ugAtrazn/L") || units.equalsIgnoreCase("ug/m3")){
             endUnits = "kg/day";//= mg/l*cfs
         }else if(units.equalsIgnoreCase("ml/l")){
             endUnits = "ml/day";//= mg/l*cfs
         }else if(units.equalsIgnoreCase("pfu/100L") || units.equalsIgnoreCase("pfu/100ml")){
             endUnits = "pfu/day";
         }else if(units.equalsIgnoreCase("ocyst/100L")){
             endUnits = "ocyst/day";
         }else if(units.equalsIgnoreCase("oocyst/10L")){
             endUnits = "oocyst/day";
         }
         return endUnits;
     }
    /**
     * 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();
    }
}