guiRegionalFDC_Model.java [src/java/cfa] Revision: 3304d5a6e69bcd69b7275e101bb29fd785d71a8a  Date: Wed Nov 12 14:24:00 MST 2014
package cfa;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Stroke;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.LogarithmicAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

/**
* Last Updated: 7-November-2014
* @author Tyler Wible
* @since 23-October-2011
*/
public class guiRegionalFDC_Model {
    //Inputs
    String mainFolder = "C:/Projects/TylerWible/CodeDirectories/NetBeans/CSIP/data/CFA/DurationCurve";
    String[] databases =  {"Temp_1", "Temp_2"};
    String[] stationIDs = {"RiverID_1", "RiverID_2"};
    String[] stationNames = {"RiverName_1", "RiverName_2"};
    String normalizingMetricName = "Long-Term Average Flow [cfs]";//"Drainage Area [Sq. Mi.]";//
    double[] normalizingMetrics = {391.2, 3276.33};  //a list of normalizing metrics (drainage area, Q2, etc.; set by 'setNormalizingMetrics')
    double[][] fdcData = {{0,550, 0,4052},           //a matrix of matched columns of flow duration curve exceedences and assosicated flows
                          {25,380, 20,3706},         //(set by 'setFlowDurationCurves')
                          {50,376, 40,3214},
                          {75,350, 60,3191},
                          {100,300, 80,2995},
                          {-1,-1, 100,2500}};
    //Outputs
    String summaryTable = "?";
    String regionalFDC = "?";
    String units = "?";
    
    //Gets
    public File getRegionalFDC_results() {
        return new File(mainFolder, "regionalfdc_results.txt");
    }
    public String getGraph() {
        return "regionalfdc_graph.jpg";
    }
    public File getRegionalFDCgraphOutput(){
        //This output file is for use with JSHighCharts
        return new File(mainFolder, "regionalfdc_graph.out");
    }
    public String getSummaryTable(){
        return summaryTable;
    }
    public String getRegionalFDC(){
        return regionalFDC;
    }
    public String getUnits(){
        return units;
    }
    
    //Sets
    public void setMainFolder(String mainFolder) {
        this.mainFolder = mainFolder;
    }
    public void setDatabase(String databaseList) {//Break up the inputs per flow duration curve
        this.databases = databaseList.split("\t");
    }
    public void setStationID(String stationIDlist) {//Break up the inputs per flow duration curve
        this.stationIDs = stationIDlist.split("\t");
    }
    public void setStationName(String stationNameList) {//Break up the inputs per flow duration curve
        this.stationNames = stationNameList.split("\t");
    }
    public void setNormalizingMetricName(String normalizingMetricName){
        this.normalizingMetricName = normalizingMetricName;
    }
    public void setNormalizingMetrics(String normalizingMetrics) throws IOException{
        //Break up formatted cross-section points and create a double array for easier use
        String[] metricsList = normalizingMetrics.split("\t");
        //If the list of points is of sufficient size and format, store it
        double[] metricsList_new = new double[metricsList.length];
        for(int i=0; i<metricsList.length; i++){
            metricsList_new[i] = Double.parseDouble(metricsList[i]);
        }

        this.normalizingMetrics = metricsList_new;
    }
    public void setFlowDurationCurves(String flowDurationCurves) throws IOException{
        //Break up formatted cross-section points and create a double array for easier use
        String[] fdcDataList = flowDurationCurves.split("\n");
        if(fdcDataList.length > 1){
            //If the list of points is of sufficient size and format, store it
            int columnSize = fdcDataList[0].split("\t").length;
            double[][] fdcData_new = new double[fdcDataList.length - 1][columnSize];
            for(int i=1; i<fdcDataList.length; i++){//Skip headder
                String[] fdc_row = fdcDataList[i].split("\t");
                for(int j=0; j<fdc_row.length; j++){
                    fdcData_new[i-1][j] = Double.parseDouble(fdc_row[j]);
                }
            }
            
            this.fdcData = fdcData_new;
        }else{
            //if the list is empty or formatted wrong, thrown an error
            ArrayList<String> errorMessage =  new ArrayList<String>();
            errorMessage.add("The list of flow duration curves provided is not in the expected tab-delimited format like: 'fdc_1\tflow_1\tfdc_2\tflow_2\n0\t550\t0\t4052\n25\t380\t20\t3706\n50\t376\t40\t3214\n75\t350\t60\t3191\n100\t300\t80\t2995\n-1\t-1\t100\t2500\n'");
            writeError(errorMessage);
        }
    }
    /**
     * Calculates a summary table of standardized percentiles based on the data,
     * then takes the percentile summary and adds it to the summary table
     * @param xyRanks  a double[][] array with column1 = x (0-100 percentages), column2 = values
     * @param firstEntry  a boolean, if true it adds a header and extra column 
     * to the beginning of the summary table
     * @param stationIndex  the index of the current station, used to pull the 
     * proper stationID the header label
     * @return  a double[] of the flows associated with the current percentile
     */
    private void calculatePercentileSummmary(double[][] xyRanks, boolean firstEntry, int stationIndex){
        DoubleMath doubleMath = new DoubleMath();
        DoubleArray doubleArray = new DoubleArray();
        //Hard coded percentile summary table
        double[] frqDisp = {99, 95, 90, 75, 50, 25, 10, 5, 1};
        
        //Interpolate desired percentiles
        double[] currentFDC = doubleArray.getColumn(xyRanks, 0);
        double[] currentFlows = doubleArray.getColumn(xyRanks, 1);
        double[] currentSummary = doubleMath.linearInterpolation(currentFDC, currentFlows, frqDisp);
        currentSummary = doubleMath.roundColumn(currentSummary, 100);
        
        //Determine if this is the first entry and should thus include the exceedance percentile summary values
        if(firstEntry){
            this.summaryTable = "Exceedance Percentile\t%\t99\t95\t90\t75\t50\t25\t10\t5\t1";
            this.summaryTable = this.summaryTable + "\nRegional FDC\tDaily Average Flow [cfs]/" + normalizingMetricName;
        }else{
            this.summaryTable = this.summaryTable + databases[stationIndex] + "; " + stationIDs[stationIndex] + "\tDaily Average Flow [cfs]/" + normalizingMetricName;
        }
        
        //Append current summary to resultSummary
        for(int j=0; j<currentSummary.length; j++){
            this.summaryTable = this.summaryTable + "\t" + String.valueOf(currentSummary[j]);
        }
        this.summaryTable = this.summaryTable + "\n";
    }
    /**
     * Take the current flow duration curve values and divide them by the 'normalizingMetric' 
     * and return the result as a double[][] with the second column as the associated flow values
     * @param currentFDC  a list of flow duration curve excedances (0-100), any "-1" will be ignored
     * @param currentFlows  a list of flow values associated with the duration curve excedances above
     * @param normalizingMetric  the value to normalize the flow duration curve by (i.e. drainage area, Q2, or others)
     * @return a double[][] with the first column as the 'currentFDC' values divided by the 'normalizingMetric' and 
     * the second column as the associated flow values 'currentFlows'
     */
    private double[][] normalizeFDCdata(double[] currentFDC, double[] currentFlows, double normalizingMetric){
        //Divide FDC flows by the normalizingMetric (drainage area, Q2, long-term average, etc)
        ArrayList<Double> modifiedFDC = new ArrayList<Double>();
        ArrayList<Double> modifiedFlows = new ArrayList<Double>();
        for(int i=0; i<currentFDC.length; i++){
            if(currentFDC[i] != -1){
                modifiedFDC.add(currentFDC[i]);
                modifiedFlows.add(currentFlows[i] / normalizingMetric);
            }
        }
        
        //Convert data back into a double[][] array
        double[][] resultData = new double[modifiedFDC.size()][2];
        for(int i=0; i<modifiedFDC.size(); i++){
            resultData[i][0] = modifiedFDC.get(i);
            resultData[i][1] = modifiedFlows.get(i);
        }
        return resultData;
    }
    /**
     * 
     * @param currentFDCdata
     * @return 
     */
    private double[] calculateWeightedFDC(double[][] currentFDCdata){
        //Determine where to interpolate the FDC
        double[] exceedances = new double[200];
        for(int i=0; i<200; i++){
            exceedances[i] = i * 0.5;
        }
        
        //Interpolate a FDC at a standard location so a weighted average of multiple of these can be made
        DoubleMath doubleMath = new DoubleMath();
        DoubleArray doubleArray = new DoubleArray();
        double[] currentFDC = doubleArray.getColumn(currentFDCdata, 0);
        double[] currentFlows = doubleArray.getColumn(currentFDCdata, 1);
        double[] summaryFDC = doubleMath.linearInterpolation(currentFDC, currentFlows, exceedances);
        
        //Multiply the resulting interpolated FDC by the it's size for a weighted average later
        double currentSize = currentFDCdata.length;
        for(int i=0; i<summaryFDC.length; i++){
            summaryFDC[i] = summaryFDC[i] * currentSize;
        }
        
        return summaryFDC;
    }
    /**
     * 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
     */
    private void writeResults(double[][] results) 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 = mainFolder + File.separator + getRegionalFDC_results().getName();
        FileWriter writer =  new FileWriter(path, false);
        PrintWriter printLine = new PrintWriter(writer);

        //Add Headers to text file
        printLine.printf("%s" + "\r\n", "Percent of Time Flow is Exceeded [%] (Excedence Probability based on Weibull Plotting Position Ranking)\tRegional FDC [" + units + "]");

        //Output data to text file
        for(int i=0; i < finalResults.size(); i++) {
                printLine.printf("%s" + "\r\n", finalResults.get(i));
        }
        System.out.println("Text File located at:\t" + path);
        printLine.close();
        writer.close();
    }
    /**
     * 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
     * @throws IOException 
     * @throws InterruptedException 
     */
    public void run() throws IOException, InterruptedException, Exception {
        DoubleArray doubleArray = new DoubleArray();
        
        //Graph the complete flow duration curve for the time period
        XYPlot plot = new XYPlot();
        //Create X Axis
        ValueAxis xAxis = new NumberAxis("Percent of Time Flow is Exceeded [%]");
        xAxis.setRange(0,100);
        plot.setDomainAxis(0, xAxis);
        //Set log-scale y axis
        this.units = "Daily Average Flow [cfs] / " + normalizingMetricName;
        LogarithmicAxis yAxis = new LogarithmicAxis(units);
        yAxis.setAllowNegativesFlag(true);
        plot.setRangeAxis(0, yAxis);
        
        //Colors in the color matrix below are: purple, blue, cyan, green, gold, red, pink
        Color[] colorMatrix = {new Color(105, 0, 255), Color.blue, Color.cyan, Color.green, new Color(255, 135, 0), new Color(255, 0, 50), new Color(255, 0, 255, 100)};
        
        //Normalize each duration curve by it's provided index/metric
        ArrayList<double[]> weightedFDC_all = new ArrayList<double[]>();
        String[][] graphData = new String[fdcData.length][fdcData[0].length];
        int stationIndex = 0;
        this.summaryTable = "";
        double totalSize = 0;
        for(int i=0; i<fdcData[0].length; i++){
            double[] currentFDC = doubleArray.getColumn(fdcData, i);
            double[] currentFlows = doubleArray.getColumn(fdcData, i+1);
            double[][] normalizedFDC = normalizeFDCdata(currentFDC, currentFlows, normalizingMetrics[stationIndex]);
            calculatePercentileSummmary(normalizedFDC, false, stationIndex);
            totalSize = totalSize + normalizedFDC.length;
            
            //Weight the current FDC by it's record length and save it to be added to the regional curve
            double[] weightedFDC = calculateWeightedFDC(normalizedFDC);
            weightedFDC_all.add(weightedFDC);
            
            //Graph new normalized FDC, and save the same data as an output for JHighcharts
            XYSeries current_FDC = new XYSeries("Normalized FDC, " + databases[stationIndex] + " Station: " + stationIDs[stationIndex] + "; " + stationNames[stationIndex]);
            for(int j=0; j<fdcData.length; j++){
                if(j < normalizedFDC.length){
                    current_FDC.add(normalizedFDC[j][0], normalizedFDC[j][1]);//x,y
                    graphData[j][i] = String.valueOf(normalizedFDC[j][0]);
                    graphData[j][i+1] = String.valueOf(normalizedFDC[j][1]);
                }else{
                    graphData[j][i] = "-1";
                    graphData[j][i+1] = "-1";
                }
            }
            //Create a graph with the FDC (line)
            XYDataset current_FDCdata = new XYSeriesCollection(current_FDC);
            XYItemRenderer renderer1 = new XYLineAndShapeRenderer(true, false);
            if(stationIndex < colorMatrix.length){
                renderer1.setSeriesPaint(0, colorMatrix[stationIndex]);
            }else{
                renderer1.setSeriesPaint(0, Color.lightGray);
            }
            //Set the FDC line data, renderer, and axis into plot
            plot.setDataset(stationIndex + 1, current_FDCdata);
            plot.setRenderer(stationIndex + 1, renderer1);
            //Map the line to the first Domain and first Range
            plot.mapDatasetToDomainAxis(stationIndex + 1, 0);
            plot.mapDatasetToRangeAxis(stationIndex + 1, 0);
            stationIndex++;
            i++;//double increase 'i' because of 1 column for fdc and 1 column for flows per station
        }
        
        //Average all of the weighted FDCs into a regional flow duration curve
        double[][] regionalXYranks = new double[200][2];
        for(int i=0; i<regionalXYranks.length; i++){
            double sum = 0;
            for(int j=0; j<weightedFDC_all.size(); j++){
                double[] currentWeightedFDC = weightedFDC_all.get(j);
                sum = sum + currentWeightedFDC[i];
            }
            regionalXYranks[i][0] = i * 0.5; //use the same exceedance values that the interpolation used
            regionalXYranks[i][1] = (sum / totalSize); //divide by total observations to get weighted average of all other fdcs base on their relative length
        }
        
        //Add the overall 'regional' summary to the beginning of the summary table
        String partialTable = this.summaryTable;
        calculatePercentileSummmary(regionalXYranks, true, -9999);
        this.summaryTable = this.summaryTable + partialTable;
        writeResults(regionalXYranks);
        
        XYSeries regional_FDC = new XYSeries("Regional Flow Duration Curve");
        this.regionalFDC = "";
        for(int j=0; j<regionalXYranks.length; j++){
            regional_FDC.add(regionalXYranks[j][0], regionalXYranks[j][1]);//x,y
            if(j == 0){
                this.regionalFDC = String.valueOf(regionalXYranks[j][0]) + "\t" + String.valueOf(regionalXYranks[j][1]);
            }else{
                this.regionalFDC = this.regionalFDC + "\n" + String.valueOf(regionalXYranks[j][0]) + "\t" + String.valueOf(regionalXYranks[j][1]);
            }
        }
        //Create a graph with the FDC (line)
        XYDataset current_FDCdata = new XYSeriesCollection(regional_FDC);
        XYItemRenderer renderer1 = new XYLineAndShapeRenderer(true, false);
        Stroke thickness = new BasicStroke(4);
        renderer1.setSeriesStroke(0, thickness);
        renderer1.setSeriesPaint(0, Color.black);
        //Set the FDC line data, renderer, and axis into plot
        plot.setDataset(0, current_FDCdata);
        plot.setRenderer(0, renderer1);
        //Map the line to the first Domain and first Range
        plot.mapDatasetToDomainAxis(0, 0);
        plot.mapDatasetToRangeAxis(0, 0);
        
        //Output XY data for use with JHighCharts
        doubleArray.writeXYseries(mainFolder, graphData, getRegionalFDCgraphOutput().getName());
        
        //Set extra graphing preferences
        Graphing graphing = new Graphing();

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

        //Graph plot onto JfreeChart
        String graphTitle = "Regionalized Flow Duration Curve, Normalized by: " + normalizingMetricName;
        JFreeChart chart = new JFreeChart(graphTitle, graphing.titleFont, plot, true);
        
        //Set legend Font
        LegendTitle legendTitle = chart.getLegend();
        legendTitle.setItemFont(graphing.masterFont);

        //Save resulting graph for proof it works
        try{
            String path = mainFolder + File.separator + getGraph();
            ChartUtilities.saveChartAsJPEG(new File(path), chart, 1280, 800);
            System.out.println("JFreeChart created properly at: " + path);
        }catch(IOException e){
            System.out.println("A problem occurred while trying to creating the chart for the regional flow duration curve. Error: USGSFDC0003");
            System.out.println(e.toString());
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException, Exception {
        //Define and run model
        guiRegionalFDC_Model fdc_Model = new guiRegionalFDC_Model();
        fdc_Model.run();
    }
}