PostgresqlGISDatabase.java [src/m/utils] Revision: default  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.utils;

import com.vividsolutions.jts.algorithm.MinimumDiameter;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
import com.vividsolutions.jts.io.WKTWriter;
import csip.api.server.ServiceException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.postgresql.util.PSQLException;

/**
 *
 * @author ktraff
 */
public class PostgresqlGISDatabase extends PostgresqlDatabase implements IGISDatabase {
    
    public static final String DEFAULT_SCHEMA = "csip_gis";
    
    public PostgresqlGISDatabase(String hostname, String port, String dbName, String username, String password) throws ServiceException {
        super(hostname, port, dbName, username, password);
    }
    
    public PostgresqlGISDatabase(Connection conn) throws Exception {
        super(conn);
    }
    
    public String getSessionID() {
        return SESSION_ID;
    }
    
    public String getDefaultCRS() {
        return Database.DEFAULT_CRS;
    }
    
    public String getDefaultSchema() {
        return DEFAULT_SCHEMA;
    }
    
    @Override
    public int deleteLayer(Layer lyr) throws ServiceException {
        return deleteTable(lyr.getSchemaTable());
    }
    
    @Override
    public Layer createLayer(String tableName, JSONObject layerJSON) throws ServiceException {
        Layer lyr = null;
        try {
            GeoJSONParser.ShapeType shapeType = GeoJSONParser.getShape(layerJSON);
            String schemaTable = getDefaultSchema() + ".\"" + tableName + "\"";
            Statement stmt = conn.createStatement();
            if (!exists(getDefaultSchema(), tableName)) {
                String sql = "CREATE TABLE " + schemaTable + " (id serial)";
                LOG.log(Level.INFO, sql);
                stmt.executeUpdate(sql);
                
                sql = "SELECT AddGeometryColumn ('" +
                        getDefaultSchema() + "','" + tableName + "', 'the_geom', " +
                        "4326, '" + shapeType + "', 2);";
                LOG.log(Level.INFO, sql);
                stmt.executeQuery(sql);
            }
            lyr = new Layer(getDefaultSchema(), tableName, this);
            
            stmt.close();
        } catch (JSONException|SQLException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
        return lyr;
    }
    
    @Override
    public Layer copyLayer(Layer lyr, String schemaName, String tableName) throws Exception {
        Table tbl = super.copyTable((Table)lyr, schemaName, tableName);
        return new Layer(tbl.schema, tbl.table, this);
    }
    
    @Override
    public Layer addGeometryColumn(Table tbl, String shapeType) throws Exception {
        Statement stmt = conn.createStatement();
        String sql = "SELECT AddGeometryColumn ('" +
                        tbl.schema + "','" + tbl.table + "', 'the_geom', " +
                        "4326, '" + shapeType + "', 2);";
        LOG.log(Level.INFO, sql);
        stmt.executeQuery(sql);

        stmt.close();
        return new Layer(tbl.schema, tbl.table, this);
    }
    
    @Override
    public Feature uploadFeature(JSONObject featureJSON, Layer layer) throws csip.api.server.ServiceException {
        try {
            Statement stmt = conn.createStatement();
            JSONObject geomJSON = featureJSON.getJSONObject("geometry");
            if (!GeoJSONParser.hasCRS(geomJSON)) {
                geomJSON.put("crs", featureJSON.get("crs"));
            }
            
            String sql = "SELECT ST_Transform(ST_GeomFromGeoJSON('" +
                    geomJSON.toString() + "'), 4326) AS geojson";
            LOG.log(Level.INFO, sql);
            ResultSet rs = stmt.executeQuery(sql);
            rs.next();
            String geom = rs.getString("geojson");
            
            // Create a mapping of ordered column values for insertion.
            ArrayList<String> cols = new ArrayList<>();
            ArrayList<Object> vals = new ArrayList();
            cols.add("the_geom");
            vals.add(geom);
            
            // Create column fields for all properties.
            if (!featureJSON.isNull("properties")) {
                ArrayList<String> columnNames = getColumns(getDefaultSchema(), layer.table);
                JSONObject props = featureJSON.getJSONObject("properties");
                Iterator<?> keys = props.keys();
                while (keys.hasNext()) {
                    String key = (String)keys.next();
                    LOG.log(Level.INFO, "Found property " + key + ": " + props.get(key));
                    cols.add(key);
                    vals.add(props.get(key));
                    if (!columnNames.contains(key)) {
                        if (props.get(key) instanceof String) {
                            addColumn(getDefaultSchema(), layer.table, key, DataType.STRING);
                        }
                        else if (props.get(key) instanceof Double ||
                                props.get(key) instanceof Integer ||
                                props.get(key) instanceof Number) {
                            addColumn(getDefaultSchema(), layer.table, key, DataType.NUMERIC);
                        }
                    }
                }
            }
            else {
                LOG.log(Level.INFO, "Found no properties");
            }
            
            sql = "INSERT INTO " + layer.getSchemaTable() + " (" + LocalStringUtils.join(cols, ",") + ") " +
                    "VALUES ('" + LocalStringUtils.join(vals, "', '")  + "') RETURNING id";
            LOG.log(Level.INFO, sql);
            stmt.execute(sql);
            ResultSet lastUpdated = stmt.getResultSet();
            int id = -1;
            if (lastUpdated.next()) {
                id = lastUpdated.getInt(1);
            }
            LOG.log(Level.INFO, "Uploaded feature with id: " + id);
            
            stmt.close();
            return new Feature(layer, featureJSON, id);
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public int findSRID(Layer lyr) throws ServiceException {
        try {
            int srid = -1;
            Statement stmt = conn.createStatement();
            String sql = "SELECT Find_SRID('" +
                    lyr.schema + "', '" +
                    lyr.table + "', 'the_geom') as srid";
            LOG.log(Level.INFO, sql);
            ResultSet rs = stmt.executeQuery(sql);
            if (rs.next()) {
                srid = rs.getInt("srid");
            }
            
            stmt.close();
            return srid;
        } catch (SQLException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public Layer intersect(Layer one, Layer two) throws Exception {
        String table = "intsn_" + getSessionID() + "_" + new RandomString(4).nextString();
        String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
        Statement stmt = conn.createStatement();

        createSchema(getDefaultSchema());
        deleteTable(schemaTable);

        ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
            "ST_Intersection(" + one.getSchemaTable() + ".the_geom, " + two.getSchemaTable() + ".the_geom) AS the_geom ",
            getProperties(one, true),
            getProperties(two, true),
        }));
        String sql = "CREATE TABLE " + schemaTable + " AS " +
                "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                "FROM " + one.getSchemaTable() + ", " + two.getSchemaTable() + " " +
                "WHERE " + one.getSchemaTable() + ".the_geom && " + two.getSchemaTable() + ".the_geom";
        LOG.log(Level.INFO, sql);
        stmt.executeUpdate(sql);

        Layer res = new Layer(getDefaultSchema(), table, this);
        updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));

        stmt.close();
        return res;
    }
    
    @Override
    public boolean coveredBy(Layer one, Layer two) throws Exception {
        boolean coveredBy = false;
        Statement stmt = conn.createStatement();

        String sql = "SELECT ST_CoveredBy(" + one.getSchemaTable() + ".the_geom, " + two.getSchemaTable() + ".the_geom) AS result " +
                     "FROM " + one.getSchemaTable() + ", " + two.getSchemaTable() + " ";
        LOG.log(Level.INFO, sql);
        ResultSet rs = stmt.executeQuery(sql);
        
        if (rs.next()) {
            coveredBy = rs.getBoolean("result");
        }

        stmt.close();
        return coveredBy;
    }
    
    @Override
    public boolean intersects(Layer one, Layer two) throws Exception {
        boolean intersects = false;
        Statement stmt = conn.createStatement();

        String sql = "SELECT ST_Intersects(" + one.getSchemaTable() + ".the_geom, " + two.getSchemaTable() + ".the_geom) " +
                     "FROM " + one.getSchemaTable() + ", " + two.getSchemaTable() + " ";
        LOG.log(Level.INFO, sql);
        ResultSet rs = stmt.executeQuery(sql);
        
        if (rs.next()) {
            intersects = rs.getBoolean("st_intersects");
        }

        stmt.close();
        return intersects;
    }
    
    @Override
    public boolean sameGeometry(Layer one, Layer two) throws Exception {
        boolean sameGeometry = false;
        Statement stmt = conn.createStatement();

        String sql = "SELECT ST_Equals(" + one.getSchemaTable() + ".the_geom, " + two.getSchemaTable() + ".the_geom) AS result " +
                     "FROM " + one.getSchemaTable() + ", " + two.getSchemaTable() + " ";
        LOG.log(Level.INFO, sql);
        ResultSet rs = stmt.executeQuery(sql);
        
        if (rs.next()) {
            sameGeometry = rs.getBoolean("result");
        }

        stmt.close();
        return sameGeometry;
    }
    
    @Override
    public Layer difference(Layer one, Layer two) throws Exception {
        return difference(one, two, new ArrayList<String>(Arrays.asList(new String[]{"id", "the_geom"})));
    }
    
    @Override
    public Layer difference(Layer one, Layer two, ArrayList<String> colsExclude) throws Exception {
        String table = "diff_" + getSessionID() + "_" + new RandomString(4).nextString();
        String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
        Statement stmt = conn.createStatement();

        createSchema(getDefaultSchema());
        deleteTable(schemaTable);

        ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
            "ST_Difference(" + one.getSchemaTable() + ".the_geom, " + two.getSchemaTable() + ".the_geom) AS the_geom ",
            getProperties(one, true, colsExclude),
            getProperties(two, true, colsExclude),
        }));
        String sql = "CREATE TABLE " + schemaTable + " AS " +
                "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                "FROM " + one.getSchemaTable() + ", " + two.getSchemaTable();
        LOG.log(Level.INFO, sql);
        stmt.executeUpdate(sql);

        Layer res = new Layer(getDefaultSchema(), table, this);
        updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));

        stmt.close();
        return res;
    }
    
    @Override
    public Layer union(Layer one, Layer two) throws ServiceException {
        try {
            String table = "union_" + getSessionID() + "_" + new RandomString(4).nextString();
            String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
            Statement stmt = conn.createStatement();
            createSchema(getDefaultSchema());
            deleteTable(schemaTable);
            
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                "ST_Union(" + one.getSchemaTable() + ".the_geom, " + two.getSchemaTable() + ".the_geom) AS the_geom ",
                getProperties(one, true),
                getProperties(two, true),
            }));
            String sql = "CREATE TABLE " + schemaTable + " AS " +
                    "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + one.getSchemaTable() + ", " + two.getSchemaTable();
            LOG.log(Level.INFO, sql);
            stmt.executeUpdate(sql);
            
            Layer res = new Layer(getDefaultSchema(), table, this);
            updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));
            
            stmt.close();
            return res;
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public Layer buffer(Layer lyr, int distance) throws ServiceException {
        try {
            String table = "buf_" + getSessionID() + "_" + new RandomString(4).nextString();
            String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
            Statement stmt = conn.createStatement();
            createSchema(getDefaultSchema());
            deleteTable(schemaTable);
            
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                "ST_Buffer(" + lyr.getSchemaTable() + ".the_geom::geography, " + distance + ")::geometry AS the_geom ",
                getProperties(lyr, true)
            }));
            String sql = "CREATE TABLE " + schemaTable + " AS " +
                    "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + lyr.getSchemaTable();
            LOG.log(Level.INFO, sql);
            stmt.executeUpdate(sql);
            
            Layer res = new Layer(getDefaultSchema(), table, this);
            updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));
            
            stmt.close();
            return res;
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public Layer minimumBoundingCircle(Layer lyr, int numSegsPerQtCirc) throws ServiceException {
        try {
            String table = "mbc_" + getSessionID() + "_" + new RandomString(4).nextString();
            String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
            Statement stmt = conn.createStatement();
            createSchema(getDefaultSchema());
            deleteTable(schemaTable);
            
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                "ST_MinimumBoundingCircle(ST_Collect(" + lyr.getSchemaTable() + ".the_geom), " +
                        numSegsPerQtCirc + ") AS the_geom ",
                getProperties(lyr, true)
            }));
            String sql = "CREATE TABLE " + schemaTable + " AS " +
                    "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + lyr.getSchemaTable();
            LOG.log(Level.INFO, sql);
            stmt.executeUpdate(sql);
            
            Layer res = new Layer(getDefaultSchema(), table, this);
            updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));
            
            stmt.close();
            return res;
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public Layer minimumBoundingRectangle(Layer lyr) throws ServiceException {
        try {
            String table = "mbr_" + getSessionID() + "_" + new RandomString(4).nextString();
            String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
            Statement stmt = conn.createStatement();
            
            JTSAdapter adapter = new JTSAdapter(this);
            String wkt = adapter.minimumBoundingRectangle(lyr);
            LOG.log(Level.INFO, "Minimum Bounding Rectangle: " + wkt);
            
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                "ST_GeomFromText('" + wkt + "') as the_geom ",
                getProperties(lyr, true)
            }));
            String sql = "CREATE TABLE " + schemaTable + " AS " +
                    "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + lyr.getSchemaTable();
            LOG.log(Level.INFO, sql);
            stmt.executeUpdate(sql);
            
            Layer res = new Layer(getDefaultSchema(), table, this);
            updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));
            
            stmt.close();
            return res;
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public Layer transform(Layer lyr, int SRID) throws ServiceException {
        try {
            String table = "tfm_" + getSessionID() + "_" + new RandomString(4).nextString();
            String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
            Statement stmt = conn.createStatement();
            
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                "ST_Transform(" + lyr.getSchemaTable() + ".the_geom, " + SRID + ") as the_geom ",
                getProperties(lyr, true)
            }));
            String sql = "CREATE TABLE " + schemaTable + " AS " +
                    "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + lyr.getSchemaTable();
            LOG.log(Level.INFO, sql);
            stmt.executeUpdate(sql);
            
            Layer res = new Layer(getDefaultSchema(), table, this);
            updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));
            
            stmt.close();
            return res;
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public Layer extent(Layer lyr) throws ServiceException {
        try {
            String table = "ext_" + getSessionID() + "_" + new RandomString(4).nextString();
            String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
            Statement stmt = conn.createStatement();
            createSchema(getDefaultSchema());
            deleteTable(schemaTable);
            
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                "ST_Extent(" + lyr.getSchemaTable() + ".the_geom)::geometry AS the_geom ",
                getProperties(lyr, true)
            }));
            String sql = "CREATE TABLE " + schemaTable + " AS " +
                    "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + lyr.getSchemaTable();
            LOG.log(Level.INFO, sql);
            stmt.executeUpdate(sql);
            
            Layer res = new Layer(getDefaultSchema(), table, this);
            updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));
            
            stmt.close();
            return res;
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public Layer envelope(Layer lyr) throws ServiceException {
        try {
            String table = "env_" + getSessionID() + "_" + new RandomString(4).nextString();
            String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
            Statement stmt = conn.createStatement();
            createSchema(getDefaultSchema());
            deleteTable(schemaTable);
            
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                "ST_Envelope(ST_Collect(" + lyr.getSchemaTable() + ".the_geom)) AS the_geom ",
                getProperties(lyr, true)
            }));
            String sql = "CREATE TABLE " + schemaTable + " AS " +
                    "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + lyr.getSchemaTable();
            LOG.log(Level.INFO, sql);
            stmt.executeUpdate(sql);
            
            Layer res = new Layer(getDefaultSchema(), table, this);
            updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));
            
            stmt.close();
            return res;
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public Layer area(Layer lyr) throws ServiceException {
        try {
            String table = "area_" + getSessionID() + "_" + new RandomString(4).nextString();
            String schemaTable = getDefaultSchema() + ".\"" + table + "\"";
            Statement stmt = conn.createStatement();
            createSchema(getDefaultSchema());
            deleteTable(schemaTable);
            
            ArrayList<String> colsExclude = new ArrayList<>(Arrays.asList(new String[]{"the_geom","id","calc_area"}));
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                lyr.getSchemaTable() + ".the_geom ",
                "ST_Area(" + lyr.getSchemaTable() + ".the_geom::geography) AS calc_area ",
                getProperties(lyr, true, colsExclude)
            }));
            String sql = "CREATE TABLE " + schemaTable + " AS " +
                    "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + lyr.getSchemaTable();
            LOG.log(Level.INFO, sql);
            stmt.executeUpdate(sql);
            
            Layer res = new Layer(getDefaultSchema(), table, this);
            updateGeometrySRID(res, GeoJSONParser.getSRIDInt(getDefaultCRS()));
            
            stmt.close();
            return res;
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    public void updateGeometrySRID(Layer lyr, int SRID) throws ServiceException {
        try {
            Statement stmt = conn.createStatement();
            String sql = "SELECT UpdateGeometrySRID('" +
                    lyr.schema + "', '" + lyr.table + "', 'the_geom', " +
                    SRID + ")";
            LOG.log(Level.INFO, sql);
            stmt.execute(sql);
        } catch (SQLException ex) {
            throw new ServiceException(ex);
        }
    }
    
    public double[] parseBBox(String bbox) {
        double [] res = new double[4];
        Pattern pattern = Pattern.compile("BOX\\(([0-9\\.-]+)\\s([0-9\\.-]+),([0-9\\.-]+) ([0-9\\.-]+)\\)");
        Matcher matcher = pattern.matcher(bbox);
        matcher.matches();
        for(int i=1; i<=matcher.groupCount(); i++){
            res[i-1] = Double.parseDouble(matcher.group(i));
        }
        return res;
    }
    
    public JSONObject toJSON(Layer lyr) throws ServiceException {
        try {
            Statement stmt = conn.createStatement();
            ArrayList<String> cols = getColumns(lyr.schema, lyr.table);
            JSONObject featureCollection = new JSONObject("{" +
                    "\"type\": \"FeatureCollection\"," +
                    "}");
            JSONArray features = new JSONArray();
            
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                "ST_AsGeoJSON(" + lyr.getSchemaTable() + ".the_geom) As geometry ",
                getProperties(lyr, false),
            }));
            String sql = "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + lyr.getSchemaTable() + " ";
            LOG.log(Level.INFO, sql);
            ResultSet rs = stmt.executeQuery(sql);
            
            while (rs.next()) {
                JSONObject feat = new JSONObject();
                feat.put("type", "Feature");
                feat.put("geometry", new JSONObject(rs.getString("geometry")));
                JSONObject jsonProps = new JSONObject();
                for (String col : cols) {
                    if (!col.equals("the_geom")) {
                        try {
                            jsonProps.put(col, rs.getObject(col));
                        } catch (PSQLException ex) {
                            LOG.log(Level.WARNING, "Could not find column: " + col + " in result set (skipping).");
                        }
                    }
                }
                feat.put("properties", jsonProps);
                features.put(feat);
            }
            
            stmt.close();
            featureCollection.put("features", features);
            return featureCollection;
        } catch (SQLException|JSONException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
    }
    
    @Override
    public String toWKT(Layer lyr) throws ServiceException {
        String wkt = "";
        try {
            Statement stmt = conn.createStatement();
            
            ArrayList<String> props = new ArrayList<>(Arrays.asList(new String[]{
                "ST_AsText(" + lyr.getSchemaTable() + ".the_geom) As wkt "
            }));
            String sql = "SELECT " + LocalStringUtils.join(props, ", ") + " " +
                    "FROM " + lyr.getSchemaTable() + " ";
            LOG.log(Level.INFO, sql);
            ResultSet rs = stmt.executeQuery(sql);
            if (rs.next()) {
                wkt = rs.getString("wkt");
            }
        } catch (SQLException ex) {
            throw new csip.api.server.ServiceException(ex);
        }
        return wkt;
    }
    
    @Override
    public JSONObject toJSON(Layer lyr, int outputSRID) throws ServiceException {
        try {
            int SRID = findSRID(lyr);
            if (SRID != outputSRID) {
                LOG.log(Level.INFO, "Translating " + lyr.toString() + " from " +
                        SRID + " to " + outputSRID);
                lyr = transform(lyr, outputSRID);
            }
            return toJSON(lyr);
        } catch (ServiceException ex) {
            throw ex;
        }
    }
    
}

/**
 * Interfaces with the Java Topology Library
 * @author ktraff
 */
class JTSAdapter {
    
    protected final Logger LOG = Logger.getLogger(getClass().getSimpleName());
    
    IGISDatabase db;
    
    public JTSAdapter(IGISDatabase db) {
        this.db = db;
    }
    
    public String minimumBoundingRectangle(Layer lyr) throws ServiceException {
        try {
            String wkt = db.toWKT(lyr);
            LOG.log(Level.INFO, "WKT: " + wkt);
            Geometry geom = new WKTReader().read(wkt);
            MinimumDiameter helper = new MinimumDiameter(geom);
            return new WKTWriter().write(helper.getMinimumRectangle());
        } catch (ParseException ex) {
            throw new ServiceException(ex);
        }
    }
    
}