package eu.esdihumboldt.hale.io.jdbc.spatialite; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.function.Supplier; import org.geotools.geometry.jts.JTS; import org.geotools.referencing.CRS; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.sqlite.SQLiteConnection; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.io.WKTReader; import com.vividsolutions.jts.io.WKTWriter; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.instance.geometry.CRSDefinitionUtil; import eu.esdihumboldt.hale.common.instance.geometry.DefaultGeometryProperty; import eu.esdihumboldt.hale.common.instance.geometry.impl.CodeDefinition; import eu.esdihumboldt.hale.common.instance.geometry.impl.WKTDefinition; import eu.esdihumboldt.hale.common.schema.geometry.CRSDefinition; import eu.esdihumboldt.hale.common.schema.geometry.GeometryProperty; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.common.schema.model.constraint.type.GeometryMetadata; import eu.esdihumboldt.hale.common.schema.model.impl.DefaultTypeDefinition; import eu.esdihumboldt.hale.io.jdbc.GeometryAdvisor; import eu.esdihumboldt.hale.io.jdbc.spatialite.internal.GeometryTypeMetadata; import eu.esdihumboldt.hale.io.jdbc.spatialite.internal.SpatiaLiteHelper; import eu.esdihumboldt.hale.io.jdbc.spatialite.internal.SpatiaLiteSupport; import eu.esdihumboldt.hale.io.jdbc.spatialite.internal.SpatiaLiteSupportFactory; import eu.esdihumboldt.hale.io.jdbc.spatialite.internal.SrsMetadata; import schemacrawler.schema.BaseColumn; import schemacrawler.schema.ColumnDataType; /** * Geometry advisor for SpatiaLite. * * @author Stefano Costa, GeoSolutions */ public class SpatiaLiteGeometries implements GeometryAdvisor<SQLiteConnection> { private static final ALogger log = ALoggerFactory.getLogger(SpatiaLiteGeometries.class); @Override public boolean isFixedType(ColumnDataType columnType) { /* * Concrete information on geometry type and SRS is not stored in the * column but as metadata in the database. Therefore every column has to * be configured on its own. */ return false; } @Override public Class<? extends Geometry> configureGeometryColumnType(SQLiteConnection connection, BaseColumn<?> column, DefaultTypeDefinition type) { String colName = column.getName(); String tabName = column.getParent().getName(); SpatiaLiteSupport slSupport = SpatiaLiteSupportFactory.getInstance() .createSpatiaLiteSupport(connection); // warn if SpatiaLite is not available SpatiaLiteHelper.isSpatialLiteLoadedReport(connection, false); GeometryTypeMetadata geomTypeMeta = slSupport.getGeometryTypeMetadata(connection, tabName, colName); if (geomTypeMeta != null) { SrsMetadata srsMeta = slSupport.getSrsMetadata(connection, geomTypeMeta.getSrid()); GeometryMetadata columnTypeConstraint; if (srsMeta != null) { // Create constraint to save the informations columnTypeConstraint = new GeometryMetadata(Integer.toString(srsMeta.getAuthSrid()), geomTypeMeta.getCoordDimension(), srsMeta.getSrText(), srsMeta.getAuthName()); } else { // no SRS information, just dimension columnTypeConstraint = new GeometryMetadata(geomTypeMeta.getCoordDimension()); } type.setConstraint(columnTypeConstraint); return geomTypeMeta.getGeomType(); } else { // no geometry column could be found return null; } } @Override public Object convertGeometry(GeometryProperty<?> geom, TypeDefinition columnType, SQLiteConnection connection) throws Exception { // show error and abort if SpatiaLite is not available if (!SpatiaLiteHelper.isSpatialLiteLoadedReport(connection, true)) { throw new IllegalStateException("SpatiaLite module is not available"); } // Transform from sourceCRS to targetCRS GeometryMetadata columnTypeMetadata = columnType.getConstraint(GeometryMetadata.class); // transform CoordinateReferenceSystem targetCRS = null; String authName = columnTypeMetadata.getAuthName(); if (authName != null && authName.equalsIgnoreCase("EPSG")) { targetCRS = CRS.decode(authName + ":" + columnTypeMetadata.getSrs()); } else { String wkt = columnTypeMetadata.getSrsText(); if (wkt != null && !wkt.isEmpty()) { targetCRS = CRS.parseWKT(wkt); } } Geometry targetGeometry; if (targetCRS != null) { MathTransform transform = CRS.findMathTransform(geom.getCRSDefinition().getCRS(), targetCRS); targetGeometry = JTS.transform(geom.getGeometry(), transform); // encode JTS Geometry return encodeGeometryValue(targetGeometry, columnTypeMetadata.getSrs(), columnTypeMetadata.getDimension(), connection); } else { targetGeometry = geom.getGeometry(); String srid = "-1"; if (geom.getCRSDefinition() != null) { // try to get SRID for source SRS String epsgCode = CRSDefinitionUtil.getEPSG(geom.getCRSDefinition()); if (epsgCode != null) { try { int epsgNumber = Integer.parseInt(epsgCode); SrsMetadata srsMeta = SpatiaLiteSupportFactory.getInstance() .createSpatiaLiteSupport(connection) .getSrsMetadata(connection, "epsg", epsgNumber); if (srsMeta != null) { srid = String.valueOf(srsMeta.getSrid()); } } catch (NumberFormatException e) { // ignore } } } // encode JTS Geometry return encodeGeometryValue(targetGeometry, srid, columnTypeMetadata.getDimension(), connection); } } private Object encodeGeometryValue(Geometry value, String srid, int dimension, SQLiteConnection connection) throws SQLException { // convert JTS geometry to SpatiaLite's internal BLOB format WKTWriter wktWriter = new WKTWriter(dimension); /* * Note: WKTWriter does produce wrong WKT (as of the OGC specification) * for 3D geometries. For example does produce "MULTIPOLGON" instead of * "MULTIPOLYGON Z". * * This is why we use the GeomFromEWKT function. See also * http://postgis. * refractions.net/docs/using_postgis_dbmanagement.html#EWKB_EWKT */ String sqlGeomFromText = "SELECT GeomFromEWKT(?)"; String sridPrefix = ""; if (srid != null) { sridPrefix = "SRID=" + srid + ";"; } PreparedStatement stmt = connection.prepareStatement(sqlGeomFromText); stmt.setString(1, sridPrefix + wktWriter.write(value)); ResultSet rs = stmt.executeQuery(); Object encodedValue = null; if (rs.next()) { encodedValue = rs.getObject(1); } return encodedValue; } /** * * @see eu.esdihumboldt.hale.io.jdbc.GeometryAdvisor#convertToInstanceGeometry(java.lang.Object, * eu.esdihumboldt.hale.common.schema.model.TypeDefinition, * java.lang.Object, java.util.function.Supplier) */ @Override public GeometryProperty<?> convertToInstanceGeometry(Object geom, TypeDefinition columnType, SQLiteConnection connection, Supplier<CRSDefinition> crsProvider) throws Exception { // show error and abort if SpatiaLite is not available if (!SpatiaLiteHelper.isSpatialLiteLoadedReport(connection, true)) { // don't throw, will prevent any data being loaded // throw new IllegalStateException("SpatiaLite module is not available"); } // decode geometry read from DB GeometryMetadata columnTypeMetadata = columnType.getConstraint(GeometryMetadata.class); Geometry jtsGeom = decodeGeometryValue(geom, columnTypeMetadata, connection); // determine CRS CRSDefinition crsDef = null; String authName = columnTypeMetadata.getAuthName(); if (authName != null && authName.equalsIgnoreCase("EPSG")) { String epsgCode = authName + ":" + columnTypeMetadata.getSrs(); crsDef = new CodeDefinition(epsgCode, null); } else { String wkt = columnTypeMetadata.getSrsText(); if (wkt != null) { crsDef = new WKTDefinition(wkt, null); } } return new DefaultGeometryProperty<Geometry>(crsDef, jtsGeom); } private Geometry decodeGeometryValue(Object geom, @SuppressWarnings("unused") GeometryMetadata metadata, SQLiteConnection connection) throws SQLException { // geom parameter is a byte[] in SpatiaLite's internal BLOB format; // for easy parsing with JTS, I must re-read geometry from DB in WKT or // WKB format /* * We could use the WKT - but the JTS WKTReader does not support * properly encoded 3D geometries (e.g. "MULTIPOLYON Z" instead of just * "MULTIPOLYGON") */ // String sqlGeomAsWKX = "SELECT ST_AsText(?)"; /* * We could use the 2D WKT - but this will reduce all 3D geometries to a * 2D projection. */ // String sqlGeomAsWKX = "SELECT AsWKT(?)"; /* * We could use the WKB - but the JTS WKBReader does not properly * support geometry type codes of 1000 and above (e.g. 1007 for a * GeometryCollection with Z coordinate) */ // String sqlGeomAsWKX = "SELECT ST_AsBinary(?)"; /* * We could use the EWKB - but the JTS WKBReader does not handle that * properly as well (wrong geometry type extracted, 3D not recognized). */ // String sqlGeomAsWKX = "SELECT AsEWKB(?)"; /* * We can use the EWKT - but the JTS WKTReader will fail if there is a * preceding SRID (which we can remove). */ String sqlGeomAsWKX = "SELECT AsEWKT(?)"; PreparedStatement stmt = connection.prepareStatement(sqlGeomAsWKX); stmt.setObject(1, geom); ResultSet rs = stmt.executeQuery(); Geometry jtsGeom = null; if (rs.next()) { // WKB // byte[] geomAsByteArray = rs.getBytes(1); // WKT String geomAsText = rs.getString(1); // remove SRID from EWKT if (geomAsText.startsWith("SRID")) { int index = geomAsText.indexOf(';'); if (index >= 0 && index + 1 < geomAsText.length()) { geomAsText = geomAsText.substring(index + 1); } } // conversion to JTS via WKB/WKT GeometryFactory factory = new GeometryFactory(); // WKBReader wkbReader = new WKBReader(factory); WKTReader wktReader = new WKTReader(factory); try { // jtsGeom = wkbReader.read(geomAsByteArray); jtsGeom = wktReader.read(geomAsText); } catch (Exception e) { log.error("Could not load geometry from database", e); } } return jtsGeom; } }