/**
* H2GIS is a library that brings spatial support to the H2 Database Engine
* <http://www.h2database.com>. H2GIS is developed by CNRS
* <http://www.cnrs.fr/>.
*
* This code is part of the H2GIS project. H2GIS is free software;
* you can redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software Foundation;
* version 3.0 of the License.
*
* H2GIS is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details <http://www.gnu.org/licenses/>.
*
*
* For more information, please consult: <http://www.h2gis.org/>
* or contact directly: info_at_h2gis.org
*/
package org.h2gis.functions.spatial.properties;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import org.h2.tools.SimpleResultSet;
import org.h2.tools.SimpleRowSource;
import org.h2gis.utilities.TableUtilities;
import org.h2gis.api.DeterministicScalarFunction;
import org.h2gis.utilities.JDBCUtilities;
import org.h2gis.utilities.SFSUtilities;
import org.h2gis.utilities.TableLocation;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* This table function explode Geometry Collection into multiple geometries
* @author Nicolas Fortin
*/
public class ST_Explode extends DeterministicScalarFunction {
/** The default field name for explode count, value is [1-n] */
public static final String EXPLODE_FIELD = "EXPLOD_ID";
public ST_Explode() {
addProperty(PROP_REMARKS, "Explode Geometry Collection into multiple geometries.\n"
+ "Note : This function supports select query as the first arfument.");
addProperty(PROP_NOBUFFER, true);
}
@Override
public String getJavaStaticMethod() {
return "explode";
}
/**
* Explode Geometry Collection into multiple geometries
* @param connection
* @param tableName the name of the input table
* @return A result set with the same content of specified table but with atomic geometries and duplicate values.
* @throws java.sql.SQLException
*/
public static ResultSet explode(Connection connection, String tableName) throws SQLException {
if(tableName.toLowerCase().startsWith("select ")){
ExplodeResultSetQuery explodeResultSetQuery = new ExplodeResultSetQuery(connection,tableName, null);
return explodeResultSetQuery.getResultSet();
}
return explode(connection, tableName,null);
}
/**
* Explode Geometry Collection into multiple geometries
* @param connection
* @param tableName the name of the input table
* @param fieldName the name of geometry field. If null the first geometry column is used.
* @return
* @throws java.sql.SQLException
*/
public static ResultSet explode(Connection connection, String tableName, String fieldName) throws SQLException {
ExplodeResultSet rowSource = new ExplodeResultSet(connection,
TableLocation.parse(tableName, JDBCUtilities.isH2DataBase(connection.getMetaData())).toString(),fieldName);
return rowSource.getResultSet();
}
/**
* Explode fields only on request
*/
public static class ExplodeResultSet implements SimpleRowSource {
// If true, table query is closed the read again
public boolean firstRow = true;
public ResultSet tableQuery;
public String tableName;
public String spatialFieldName;
public int spatialFieldIndex =-1;
public int columnCount;
public Queue<Geometry> sourceRowGeometries = new LinkedList<Geometry>();
public int explodeId = 1;
public Connection connection;
public ExplodeResultSet(Connection connection, String tableName, String spatialFieldName) {
this.tableName = tableName;
this.spatialFieldName = spatialFieldName;
this.connection = connection;
}
@Override
public Object[] readRow() throws SQLException {
if(firstRow) {
reset();
}
if(sourceRowGeometries.isEmpty()) {
parseRow();
}
if(sourceRowGeometries.isEmpty()) {
// No more rows
return null;
} else {
Object[] objects = new Object[columnCount+1];
for(int i=1;i<=columnCount+1;i++) {
if(i==spatialFieldIndex) {
objects[i-1] = sourceRowGeometries.remove();
} else if(i==columnCount+1) {
objects[i-1] = explodeId++;
} else {
objects[i-1] = tableQuery.getObject(i);
}
}
return objects;
}
}
/**
* Explode the geometry
* @param geometry
*/
private void explode(final Geometry geometry) {
if (geometry instanceof GeometryCollection) {
final int nbOfGeometries = geometry.getNumGeometries();
for (int i = 0; i < nbOfGeometries; i++) {
explode(geometry.getGeometryN(i));
}
} else {
sourceRowGeometries.add(geometry);
}
}
@Override
public void close() {
if(tableQuery!=null) {
try {
tableQuery.close();
tableQuery = null;
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
}
/**
* Read the geometry value and explode it.
* @throws SQLException
*/
private void parseRow() throws SQLException {
sourceRowGeometries.clear();
explodeId = 1;
if(tableQuery.next()) {
Geometry geometry = (Geometry) tableQuery.getObject(spatialFieldIndex);
explode(geometry);
// If the geometry is empty, set empty field or null if generic geometry collection
if(sourceRowGeometries.isEmpty()) {
GeometryFactory factory = geometry.getFactory();
if(factory==null) {
factory = new GeometryFactory();
}
if(geometry instanceof MultiLineString) {
sourceRowGeometries.add(factory.createLineString(new Coordinate[0]));
} else if(geometry instanceof MultiPolygon) {
sourceRowGeometries.add((factory.createPolygon(null,null)));
} else {
sourceRowGeometries.add(null);
}
}
}
}
@Override
public void reset() throws SQLException {
if(tableQuery!=null && !tableQuery.isClosed()) {
close();
}
Statement st = connection.createStatement();
tableQuery = st.executeQuery("SELECT * FROM "+tableName);
firstRow = false;
ResultSetMetaData meta = tableQuery.getMetaData();
columnCount = meta.getColumnCount();
if(spatialFieldName==null) {
// Find first geometry column
List<String> geomFields = SFSUtilities.getGeometryFields(connection,TableLocation.parse(tableName));
if(!geomFields.isEmpty()) {
spatialFieldName = geomFields.get(0);
} else {
throw new SQLException("The table "+tableName+" does not contain a geometry field");
}
}
for(int i=1;i<=columnCount;i++) {
if(meta.getColumnName(i).equalsIgnoreCase(spatialFieldName)) {
spatialFieldIndex = i;
break;
}
}
if(spatialFieldIndex==-1) {
throw new SQLException("Geometry field "+spatialFieldName+" of table "+tableName+" not found");
}
}
/**
* Return the exploded geometries as multiple rows
* @return
* @throws SQLException
*/
public ResultSet getResultSet() throws SQLException {
SimpleResultSet rs = new SimpleResultSet(this);
// Feed with fields
TableUtilities.copyFields(connection, rs, TableLocation.parse(tableName, JDBCUtilities.isH2DataBase(connection.getMetaData())));
rs.addColumn(EXPLODE_FIELD, Types.INTEGER,10,0);
return rs;
}
}
/**
* Explode fields only on request
* The input data must be a SELECT expression that contains a geometry column
*/
public static class ExplodeResultSetQuery extends ExplodeResultSet {
public ExplodeResultSetQuery(Connection connection, String tableName, String spatialFieldName) {
super(connection, tableName, spatialFieldName);
}
@Override
public void reset() throws SQLException {
if (tableQuery != null && !tableQuery.isClosed()) {
close();
}
Statement st = connection.createStatement();
tableQuery = st.executeQuery(tableName);
firstRow = false;
}
@Override
public ResultSet getResultSet() throws SQLException {
SimpleResultSet rs = new SimpleResultSet(this);
// Feed with fields
copyfields(rs, tableName);
rs.addColumn(EXPLODE_FIELD, Types.INTEGER,10,0);
return rs;
}
/**
* Perform a fast copy of columns using a limit clause.
* @param rs
* @param selectQuery
* @throws SQLException
*/
private void copyfields(SimpleResultSet rs, String selectQuery) throws SQLException {
Statement st = null;
ResultSet rsQuery = null;
st = connection.createStatement();
try {
rsQuery = st.executeQuery(limitQuery(selectQuery.toUpperCase()));
ResultSetMetaData metadata = rsQuery.getMetaData();
columnCount = metadata.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
if (metadata.getColumnTypeName(i).equalsIgnoreCase("geometry")&& spatialFieldIndex==-1) {
spatialFieldIndex = i;
}
rs.addColumn(metadata.getColumnName(i), metadata.getColumnType(i),
metadata.getColumnTypeName(i), metadata.getPrecision(i), metadata.getScale(i));
}
} catch (SQLException ex) {
throw new SQLException(ex);
}
if (spatialFieldIndex == -1) {
throw new SQLException("The select query " + selectQuery + " does not contain a geometry field");
}
}
/**
* Method to perform the select query with a limit clause
* @param selectQuery
* @return
*/
private String limitQuery(String selectQuery) {
int findLIMIT = selectQuery.lastIndexOf("LIMIT ");
int comma = selectQuery.lastIndexOf(";");
if (findLIMIT == -1) {
if (comma == -1) {
selectQuery += " LIMIT 0;";
} else {
selectQuery = selectQuery.substring(0, comma) + " LIMIT 0;";
}
} else {
selectQuery = selectQuery.substring(0, findLIMIT) + " LIMIT 0;";
}
return selectQuery;
}
}
}