WaterLocations_CDWR.java [src/WaterLocations] Revision: default  Date:
package WaterLocations;

import java.io.IOException;
import java.util.ArrayList;
//import java.util.regex.Matcher;
//import java.util.regex.Pattern;
import utils.WebPageUtils;

/**
 * Last Updated: 27-June-2019
 * @author Tyler Wible
 * @since 3-October-2018
 * For more information on all of CDWR's CDSS data services, go to:
 * https://dwr.state.co.us/Rest/GET/Help
 */
public class WaterLocations_CDWR implements WaterLocationsInterface {
    public String database = "CDWR";

    @Override
    public ArrayList<String> downloadStations(String minLat, String maxLat, String minLong, String maxLong, boolean keepHistoricTF) throws WaterLocationsException {
        //NOTE: CDWR uses a lat/long plus a radius (miles) search for stations by location. So we need to convert the bounding box into this.
        
        //Calculate center of bounding box for CDWR's search
        double minLatDouble = Double.parseDouble(minLat);
        double minLongDouble = Double.parseDouble(minLong);
        double maxLatDouble = Double.parseDouble(maxLat);
        double maxLongDouble = Double.parseDouble(maxLong);
        double averageLatitude = (minLatDouble + maxLatDouble) / 2.0;
        double averageLongitude = (minLongDouble + maxLongDouble) / 2.0;
        
        //Calculate distance between center (aveLat, aveLong) and a corner (minLat, minLong) of bounding box for CDWR's search
        double radiusMiles = calculateDistanceBetweenLocations(averageLatitude, minLatDouble, averageLongitude, minLongDouble);
        int radiusMilesRound = (int) Math.ceil(radiusMiles);
        
        //Specify station website from inputs
        //https://dwr.state.co.us/rest/GET/api/v2/telemetrystations/telemetrystation/?format=csv&latitude=40.4500296208208&longitude=-105.204340919673&radius=15&units=miles
        String stationUrl = "https://dwr.state.co.us/rest/GET/api/v2/telemetrystations/telemetrystation/?format=csv" +
                "&latitude=" + String.valueOf(averageLatitude) +
                "&longitude=" + String.valueOf(averageLongitude) +
                "&radius=" + String.valueOf(radiusMilesRound) +
                "&units=miles";
        if(keepHistoricTF){
            stationUrl += "&includeHistoric=true";
        }
        
        //Fetch the monitoring stations webpage for current bounding box
        ArrayList<String> webpageAll = new ArrayList<>();
        try {
            webpageAll = WebPageUtils.downloadWebpage(stationUrl);
        } catch (IOException ex) {
            throw new WaterLocationsException("The was an issue extracting " + database + " station locations from the specified URl: " + stationUrl + ". " + ex.getMessage());
        }
        return webpageAll;
    }

    @Override
    public ArrayList<Station> getStations(String minLat, String maxLat, String minLong, String maxLong, boolean keepHistoricTF) throws WaterLocationsException {
        //Fetch monitoring stations web page for current bounding box
        ArrayList<String> webpageAll = downloadStations(minLat, maxLat, minLong, maxLong, keepHistoricTF);
        
        ArrayList<Station> stations = new ArrayList();
        for(int i=3; i<webpageAll.size(); i++){//Skip header line
            String[] columns = webpageAll.get(i).split("\\,");
//            String[] temp = splitWithQuotes(webpageAll.get(i));
//            String[] temp2 = splitWithQuotes(webpageAll.get(i), ",", false);
//            String[] temp4 = splitWithQuotes(webpageAll.get(i), "\\,", false);

            //Due to extra commas in the organization names, look for which column is "active/historic"
            int index = 11;
            for(int j=0; j<columns.length; j++){
                if(columns[j].equalsIgnoreCase("active") || columns[j].equalsIgnoreCase("historic")){
                    index = j;
                }
            }
            int extraNameColumns = index - 11;

            //Keep out only the desired data
            //columns[3] = Station Name
            //columns[4] = Org. ID
            //columns[5] = Org. Name
            //columns[9] = Station ID (abbreviation)
            //columns[11] = station status (active, historic)
            //columns[12] = Station Type (Diversion Structure, Stream Gage, Storage Structure)
            //columns[22] = Drainage Area
            //columns[23] = HUC10
            //columns[26] = Latitude
            //columns[27] = Longitude
            Station currentStation = new Station(database + "-" + columns[9]);
            currentStation.SetStationName(columns[3]);
            currentStation.SetOrgId(columns[4]);
            currentStation.SetOrgName(columns[5]);
            currentStation.SetStationId(columns[index - 2]);
            currentStation.SetStationType(columns[index + 1]);
            currentStation.SetDrainageArea(columns[index + 11]);
            currentStation.SetHUC(columns[index + 12]);
            currentStation.SetLatitude(columns[index + 15]);
            currentStation.SetLongitude(columns[index + 16]);
            boolean realTimeDataFlag = false;
            if(columns[index].equalsIgnoreCase("active")){
                realTimeDataFlag = true;
            }
            currentStation.SetRealTimeDataFlag(realTimeDataFlag);
            
            if(extraNameColumns > 0){
                String currentName = columns[5];
                for(int j=1;j<=extraNameColumns; j++){
                    currentName += columns[5 + j];
                }
                currentStation.SetOrgName(currentName);
            }
            stations.add(currentStation);
        }
        return stations;
    }
    
//    private static String[] splitWithQuotes(String text){
//        ArrayList<String> list = new ArrayList<>();
//        Matcher m = Pattern.compile( "([^\"]\\S*|\".+?\")\\s*" ).matcher( text );
//
//        while(m.find()){
//            list.add( m.group( 1 ) ); // Add .replace("\"", "") to remove surrounding quotes.
//        }
//
//        return list.toArray( new String[0] );
//    }
//    /**
//     *
//     * @param text The text you want to split
//     * @param delimiter The character or characters to split the text on
//     * @param ignoreDuplicates <pre>Allows for multiple delimiters in a row acting as one delimiter.
//     * This most often happens when " " (space) is the delimiter, used in human readable files.
//     * Two spaces in a row is effectively the same as one space.
//     *
//     * In a csv file, on the other hand, multiple delimiters often mean a null value.
//     * ignoreDuplicates would be true in the case of spaces, false in the case of commas.</pre>
//     *
//     * @return
//     */
//    public static String[] splitWithQuotes( String text, String delimiter, boolean ignoreDuplicates )
//    {
//        if(delimiter.equalsIgnoreCase( " " )){
//            delimiter = "\\s";
//        }
//        ArrayList<String> list = new ArrayList<>();
//        String pattern = "([^\"]\\S*|\".+?\")" + delimiter;
//        if(ignoreDuplicates){
//            pattern += "*";
//        }
//
//        Matcher m = Pattern.compile( pattern ).matcher( text );
//
//        while(m.find()){
//            list.add( m.group( 1 ) );
//        }
//
//        return list.toArray( new String[0] );
//    }
    
    private double calculateDistanceBetweenLocations(double latitude1, double latitude2, double longitude1, double longitude2){
        //This uses the ‘haversine’ formula to calculate the great-circle distance between two points – that is, the shortest distance over the earth’s surface – giving an ‘as-the-crow-flies’ distance between the points.
        //
        //Haversine formula: a = sin^2(deltapsi/2) + cos(psi1) * cos(psi2) * sin²(deltalambda/2)
        //c = 2 * atan2( sqrt(a), sqrt(1−a) )
        //d = R * c
        //where psi is latitude, lambda is longitude, R is earth’s radius (mean radius = 6,371km);
        //Note: angles need to be in radians to work with trig functions

        double R = 6371 * Math.pow(10, 3);//earth's radius in metres
        double psi1 = degreesToRadians(latitude1);
        double psi2 = degreesToRadians(latitude2);
        double deltaPsi = degreesToRadians(latitude2-latitude1);
        double deltaLambda = degreesToRadians(longitude2-longitude1);

        double a = Math.sin(deltaPsi/2) * Math.sin(deltaPsi/2) + Math.cos(psi1) * Math.cos(psi2) * Math.sin(deltaLambda/2) * Math.sin(deltaLambda/2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));

        double distanceMeters = R * c;
        double distanceMiles = distanceMeters * (0.000621371);
        return distanceMiles;
    }
    
    private double degreesToRadians(double degrees){
        double radians = degrees * Math.PI / 180.0;
        return radians;
    }
}