PGTools.java [src/java/m/rse/cfactor/utils] Revision:   Date:
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package m.rse.cfactor.utils;

import csip.Config;
import csip.ServiceException;
import csip.SessionLogger;
import csip.utils.Binaries;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import javax.sql.DataSource;
import java.util.ArrayList;
import static m.rse.cfactor.utils.Const.RSE_DEPLOYMENT_LINUX;
import org.apache.tomcat.jdbc.pool.PoolProperties;

/**
 *
 * @author LYaege
 * @author Shaun Case
 * @author Wes Lloyd
 */
public class PGTools {

    private static ConcurrentHashMap<String, DataSource> dataSources = new ConcurrentHashMap<>();
    private static ConcurrentHashMap<String, String> connectionStrings = new ConcurrentHashMap<>();

    public static synchronized Connection getConnection(String className, SessionLogger logger) throws csip.ServiceException {
        Connection ret_val = null;
        boolean debugging = Config.getBoolean("debugging", false);

        if (null != className) {

            //Don't need to synchronize() around these calls because they are already in a synchronized member
            if (!connectionStrings.containsKey(className)) {
                logger.log(Level.INFO, "Using database source name: {0}", className);
                String tConnectionString = Config.getString((debugging ? className.toLowerCase() + ".debug.db" : className.toLowerCase() + ".db"), "error");
                if ((null != tConnectionString) && !tConnectionString.equalsIgnoreCase("error")) {
                    logger.log(Level.INFO, "Using the connection string: {0}", tConnectionString);
                    connectionStrings.put(className, tConnectionString);
                } else {
                    logger.log(Level.SEVERE, "No connection string found for the database source name of: {0}.  The connection string should have been located under key: {1}. ", new Object[]{className, debugging ? className.toLowerCase() + ".debug.db" : className.toLowerCase() + ".db"});
                    throw new csip.ServiceException("No connection configuration string found for the database source name of: " + className + ". The connection string should have been located under key: " + (debugging ? className.toLowerCase() + ".debug.db" : className.toLowerCase() + ".db") + ". ");
                }
            }

            if (connectionStrings.get(className) == null || connectionStrings.get(className).isEmpty()) {
                if (logger != null) {
                    logger.log(Level.SEVERE, "Connection string not provided.  Please configure the {0} configuration parameter.", (debugging ? className.toLowerCase() + ".debug.db" : className.toLowerCase() + ".db"));
                }
                throw new csip.ServiceException("Unable to connect to RSE database.  Please check the " + (debugging ? className.toLowerCase() + ".debug.db" : className.toLowerCase() + ".db") + " configuration parameter");
            }

            try {
                if (!dataSources.containsKey(className)) {
                    String tConnectionDriver = Config.getString(className + ".db.driver", "error");
                    if ((null != tConnectionDriver) && !tConnectionDriver.equalsIgnoreCase("error")) {
                        logger.log(Level.INFO, "Using the connection driver: {0}", tConnectionDriver);
                        createDataSource(className, tConnectionDriver, logger);
                    } else {
                        logger.log(Level.SEVERE, "Cannot find the connection driver configuration key for {0}.db.driver .  Cannot proceed to making a database connection.", className);
                        throw new csip.ServiceException("Cannot find the connection driver configuration key for " + className + ".db.driver .  Cannot proceed to making a database connection.");
                    }
                }

                ret_val = dataSources.get(className).getConnection();
            } catch (SQLException ex) {
                if (logger != null) {
                    logger.log(Level.SEVERE, null, ex);
                }
                throw new csip.ServiceException("Failed to connect to RSE database.  Please check the " + className + " configuration parameter");
            }
        }
        return ret_val;
    }

    //Doesn't 'need' to be synchronized because it should only be called by getConnection() which is synchronized, 
    //  but just in case someone tries calling it elsewhere within this class at a later date...leave it synchronized.
    private static synchronized void createDataSource(String className, String DriverClassName, SessionLogger logger) throws SQLException {
        PoolProperties p = new PoolProperties();
        p.setUrl(connectionStrings.get(className));
        p.setDriverClassName(DriverClassName);
        p.setDefaultAutoCommit(false);
        p.setJmxEnabled(false);
        p.setTestOnBorrow(true);
        p.setValidationQuery("SELECT 1");
        p.setTestOnReturn(false);
        p.setValidationInterval(30000);
        p.setTimeBetweenEvictionRunsMillis(30000);
        p.setMaxWait(10000);
        p.setRemoveAbandonedTimeout(10);
        p.setRemoveAbandoned(true);
        p.setJdbcInterceptors("org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;"
                + "org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer;"
                + "org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer");

        dataSources.put(className, new org.apache.tomcat.jdbc.pool.DataSource(p));
        if (logger != null) {
            logger.log(Level.INFO, "\nCreated datasource {0}", dataSources.get(className).toString());
        }
    }

    public static class PolygonLatLon {

        private ArrayList<PGTools.PolygonLatLon.PointLatLon> points;
        private Boolean bValid;

        public enum ESRIContourType {

            outer, inner, unknown
        };

        protected ESRIContourType myRotation;

        public PolygonLatLon() {
            this.bValid = true;
            this.points = new ArrayList<>();
        }

        public PolygonLatLon(ArrayList<PGTools.PolygonLatLon.PointLatLon> tPoints) {
            if (null != tPoints) {
                this.bValid = true;
                this.points = new ArrayList<>(tPoints);
                for (PGTools.PolygonLatLon.PointLatLon tPoint : tPoints) {
                    if (!tPoint.isValid()) {
                        this.bValid = false;
                        break;
                    }
                }
            }
        }

        public void add(PGTools.PolygonLatLon.PointLatLon tPoint) {
            this.points.add(tPoint);
            if (!tPoint.isValid()) {
                this.bValid = false;
            }
        }

        public void add(Double Lat, Double Lon) {
            PGTools.PolygonLatLon.PointLatLon tPoint = new PGTools.PolygonLatLon.PointLatLon(Lat, Lon);
            this.points.add(tPoint);
            if (!tPoint.isValid()) {
                this.bValid = false;
            }
        }

        public String toWKT() {
            String ret_val = "'POLYGON((";
            int currentPoint = 0;
            if (this.bValid) {
                for (PGTools.PolygonLatLon.PointLatLon tPoint : this.points) {
                    if (currentPoint > 0) {
                        ret_val += ", ";
                    }
                    //  Remember Lon is X and Lat is Y...
                    ret_val += tPoint.getLon().toString() + " " + tPoint.getLat().toString();
                    currentPoint++;
                }

                ret_val += "))'";
            } else {
                ret_val = "";
            }

            return ret_val;
        }

        public Boolean isValid() {
            this.getRotation();
            return this.bValid;
        }

        protected void swapRotation() {
            ArrayList<PointLatLon> newPoints = new ArrayList<>();
            int j;

            for (j = this.points.size() - 1; j >= 0; j--) {
                PointLatLon tPoint = new PointLatLon(this.points.get(j).getLat(), this.points.get(j).getLon());
                newPoints.add(tPoint);
            }

            this.points.clear();
            this.points = newPoints;
        }

        public Boolean makeESRIRotation(ESRIContourType contourType) {
            Boolean ret_val = false;
            if (this.isValid()) {
                if (((contourType == ESRIContourType.outer) && (this.myRotation == ESRIContourType.inner))
                        || ((contourType == ESRIContourType.inner) && (this.myRotation == ESRIContourType.outer))) {
                    this.swapRotation();
                }
                ret_val = true;
            }

            return ret_val;
        }

        protected void getRotation() {
            double contour_area = 0.0;
            int j;
            PointLatLon a, b;

            this.myRotation = ESRIContourType.unknown;

            if (this.bValid) {

                //  Calculate cartesian area...test for positive or negative value.  This tells the rotation of points CW versus CCW.
                //  NOTE:  Don't use this method to calc "real" areas of Lat/Lon!  This is just a fast way of finding out the order of points.
                //         Lat/Lon must be projected first to get actual area using this type of cross-product method.
                if (this.points.size() > 2) {
                    for (j = 0; j < this.points.size() - 2; j++) {
                        a = this.points.get(j);
                        b = this.points.get(j + 1);

                        contour_area += a.getLon() * b.getLat() - b.getLon() * a.getLat();
                    }
                    a = this.points.get(j);
                    b = this.points.get(0);
                    contour_area += a.getLon() * b.getLat() - b.getLon() * a.getLat();

                    if (contour_area < 0) {
                        this.myRotation = ESRIContourType.inner;  //CCW		    
                    } else {
                        this.myRotation = ESRIContourType.outer; //CW		    
                    }
                }
            }
        }

        public static class PointLatLon {

            private Double Lat;
            private Double Lon;
            private Boolean bValid;

            public PointLatLon(Double Lat, Double Lon) {
                this.Lat = Lat;
                this.Lon = Lon;
                this.bValid = true;

                if (Math.abs(Lon) > 180) {
                    this.bValid = false;
                }

                if (Math.abs(Lat) > 90) {
                    this.bValid = false;
                }
            }

            public Boolean isValid() {
                return this.bValid;
            }

            public Double getLat() {
                return this.Lat;
            }

            public Double getLon() {
                return this.Lon;
            }

            public void setLon(Double Lon) {
                this.Lon = Lon;
                if (Math.abs(Lon) > 180) {
                    this.bValid = false;
                }
            }

            public void setLat(Double Lat) {
                this.Lat = Lat;
                if (Math.abs(Lat) > 90) {
                    this.bValid = false;
                }
            }
        }
    }

    /**
     * @author Shaun Case
     * @param polygon
     * @return
     * @throws SQLException
     */
    public static Centroid getCentroid(String polygon, SessionLogger logger) throws SQLException, ServiceException {
        Centroid cent = new Centroid();
        try (Connection c = PGTools.getConnection("cfactor", logger)) {
            try (Statement s = c.createStatement()) {
                String find_centroid = "select (" + polygon + ".STCentroid().STY) as lat, "
                        + "(" + polygon + ".STCentroid().STX) as long;";
                try (ResultSet r = s.executeQuery(find_centroid)) {
                    logger.info("query sql=" + find_centroid);
                    if (r.getMetaData().getColumnCount() != 2) {
                        logger.severe("invalid columns in getCentroid query");
                        throw new ServiceException("invalid columns in getCentroid query");
                    }
                    logger.info("getting centroid for polygon");
                    if (r.next()) {
                        cent.lat = r.getString(1);
                        cent.lon = r.getString(2);
                    }
                }
            }
        }
        return cent;
    }

    public static class Centroid {

        public String lon = "0.0";
        public String lat = "0.0";
    }

    /**
     * @author Wes Lloyd
     * @param slat
     * @param slon
     * @param workspacedirectory
     * @return
     * @throws SQLException
     * @throws ServiceException
     * @throws IOException Synchronized in an attempt to reduce random shell
     * failures...
     */
    public static synchronized double getCFactorRaster(String slat, String slon, File workspacedirectory, SessionLogger logger) throws SQLException, ServiceException, IOException {
        double c_fact = Const.UNKNOWN_CFACTOR;

        //File tiffFile = new File("/tmp/us_cvalues_topo2ras_masked.tif");
        String gdal = Config.getString("gdal.deployment", RSE_DEPLOYMENT_LINUX);
        String gdalwinpath = Config.getString("gdalwinpath", "C:\\Program Files\\GDAL\\");
        String gdalbinpath = Config.getString("gdalbinpath", gdalwinpath);
        String tifptr = "";

        if (gdal.equals(Const.RSE_DEPLOYMENT_WINDOWS)) {
            // Extract GDAL Library for Windows
            //            
            Binaries.unpackResourceAbsolute("/us_cvalues_topo2ras_masked.tfw", gdalwinpath + "us_cvalues_topo2ras_masked.tfw").toString();
            tifptr = Binaries.unpackResourceAbsolute("/us_cvalues_topo2ras_masked.tif", gdalwinpath + "us_cvalues_topo2ras_masked.tif").toString();
            Binaries.unpackResourceAbsolute("/us_cvalues_topo2ras_masked.tif.aux.xml", gdalwinpath + "us_cvalues_topo2ras_masked.tif.aux.xml").toString();
            logger.info("geotiff=" + tifptr);
            // GDAL required DLLs & EXE
            Binaries.unpackResourceAbsolute("/gdallocationinfo.exe", gdalwinpath + "gdallocationinfo.exe").toString();
            Binaries.unpackResourceAbsolute("/gdal111.dll", gdalwinpath + "gdal111.dll").toString();
            Binaries.unpackResourceAbsolute("/geos.dll", gdalwinpath + "geos.dll").toString();
            Binaries.unpackResourceAbsolute("/geos_c.dll", gdalwinpath + "geos_c.dll").toString();
            Binaries.unpackResourceAbsolute("/iconv.dll", gdalwinpath + "iconv.dll").toString();
            Binaries.unpackResourceAbsolute("/libcurl.dll", gdalwinpath + "libcurl.dll").toString();
            Binaries.unpackResourceAbsolute("/libeay32.dll", gdalwinpath + "libeay32.dll").toString();
            Binaries.unpackResourceAbsolute("/libexpat.dll", gdalwinpath + "libexpat.dll").toString();
            Binaries.unpackResourceAbsolute("/libmysql.dll", gdalwinpath + "libmysql.dll").toString();
            Binaries.unpackResourceAbsolute("/libpq.dll", gdalwinpath + "libpq.dll").toString();
            Binaries.unpackResourceAbsolute("/openjp2.dll", gdalwinpath + "openjp2.dll").toString();
            Binaries.unpackResourceAbsolute("/proj.dll", gdalwinpath + "proj.dll").toString();
            Binaries.unpackResourceAbsolute("/spatialite.dll", gdalwinpath + "spatialite.dll").toString();
            Binaries.unpackResourceAbsolute("/ssleay32.dll", gdalwinpath + "ssleay32.dll").toString();
            Binaries.unpackResourceAbsolute("/xerces-c_2_8.dll", gdalwinpath + "xerces-c_2_8.dll").toString();
            Binaries.unpackResourceAbsolute("/zlib1.dll", gdalwinpath + "zlib1.dll").toString();

            // Microsoft Visual C++ Redistributable Package 2010 - Service Pack 1 DLLs
            Binaries.unpackResourceAbsolute("/atl100.dll", gdalwinpath + "atl100.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100chs.dll", gdalwinpath + "mfc100chs.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100cht.dll", gdalwinpath + "mfc100cht.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100deu.dll", gdalwinpath + "mfc100deu.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100.dll", gdalwinpath + "mfc100.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100enu.dll", gdalwinpath + "mfc100enu.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100esn.dll", gdalwinpath + "mfc100esn.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100fra.dll", gdalwinpath + "mfc100fra.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100ita.dll", gdalwinpath + "mfc100ita.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100jpn.dll", gdalwinpath + "mfc100jpn.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100kor.dll", gdalwinpath + "mfc100kor.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100rus.dll", gdalwinpath + "mfc100rus.dll").toString();
            Binaries.unpackResourceAbsolute("/mfc100u.dll", gdalwinpath + "mfc100u.dll").toString();
            Binaries.unpackResourceAbsolute("/mfcm100.dll", gdalwinpath + "mfcm100.dll").toString();
            Binaries.unpackResourceAbsolute("/mfcm100u.dll", gdalwinpath + "mfcm100u.dll").toString();
            Binaries.unpackResourceAbsolute("/msvcp100.dll", gdalwinpath + "msvcp100.dll").toString();
            Binaries.unpackResourceAbsolute("/msvcr100.dll", gdalwinpath + "msvcr100.dll").toString();
            Binaries.unpackResourceAbsolute("/propsys.dll", gdalwinpath + "propsys.dll").toString();
            Binaries.unpackResourceAbsolute("/vcomp100.dll", gdalwinpath + "vcomp100.dll").toString();
        } else {

            //Binaries.unpackResourceAbsolute("/us_cvalues_topo2ras_masked.tfw", workspacedirectory + "/us_cvalues_topo2ras_masked.tfw").toString();
            tifptr = workspacedirectory + "/us_cvalues_topo2ras_masked.tif"; //Binaries.unpackResourceAbsolute("/us_cvalues_topo2ras_masked.tif", workspacedirectory + "/us_cvalues_topo2ras_masked.tif").toString();
            //Binaries.unpackResourceAbsolute("/us_cvalues_topo2ras_masked.tif.aux.xml", workspacedirectory + "/us_cvalues_topo2ras_masked.tif.aux.xml").toString();
            logger.info("geotiff=" + tifptr);
        }

        // to do
        // linux based WEPS assumes gdal is already installed
        // optionally we might bundle the GDAL files for Linux here as well
        //pc.exe = (gdal.equals(Const.RSE_DEPLOYMENT_WINDOWS) ? gdalwinpath : "") + "gdallocationinfo";
        logger.info("gdal is equal to=" + gdal);

        Execution e = new Execution(new File(gdalbinpath + "gdallocationinfo"));
        e.setArguments(tifptr, slon, slat, "-wgs84");
        e.setWorkingDirectory(workspacedirectory);
        e.setLogger(logger);

        StringWriter stdout = new StringWriter();
        StringWriter stderr = new StringWriter();
        e.redirectOutput(stdout);
        e.redirectError(stderr);
        int res = e.exec();
        logger.info("RET=" + res);
        logger.info("STDOUT=" + stdout.toString());
        logger.info("STDERR=" + stderr.toString());

        String output[] = stdout.toString().split("\\r?\\n");

//        pc.exe = (gdal.equalsIgnoreCase(Const.RSE_DEPLOYMENT_WINDOWS) ? gdalwinpath : "") + "gdallocationinfo";
//        logger.info(" the pc.exe is=" + pc.exe);
//        pc.args = new String[]{tifptr, slon, slat, "-wgs84"};
//        File workDir = workspacedirectory;
//        String sessionWorkDir = workDir.toString();
//        pc.working_dir = sessionWorkDir;
//        pc.execute();
//        logger.info("STDOUT=" + pc.stdout);
//        logger.info("STDERR=" + pc.stderr);
//        logger.info("working dir=" + pc.working_dir);
//        logger.info("exe=" + pc.exe);
//        String output[] = pc.stdout.split("\\r?\\n");
        // cvalue appears on last line of output
        //for (int i=0; i<output.length; i++)
        //    logger.info("i=" +i + " str=" + output[i]);
        if (res == 0 && output.length > 0) {
            String val[];
            val = output[output.length - 1].split(" ");
            //for (int i=0; i<val.length; i++)
            //    logger.info("i=" +i + " str=" + val[i]);
            // c value is in the last position (col) of the last line  
            if ((null != val) && (val.length > 0) && !val[val.length - 1].isEmpty()) {
                c_fact = Double.parseDouble(val[val.length - 1]);
            } else {
                logger.severe("gdallocationinfo returned no parseable output.  Cannot proceed. ");
                throw new csip.ServiceException("Cannot proceed with cfactor calculation; gdallocationinfo returned no parseable output: " + stdout.toString() + " err " + stderr.toString());
            }
        } else {
            logger.severe("gdallocationinfo returned no output on stdout.  Cannot proceed. ");
            throw new csip.ServiceException("Cannot proceed with cfactor calculation: " + stderr.toString());
        }
        return c_fact;
    }

}