diff --git a/core/build.gradle b/core/build.gradle index af6886fc48..0ec51b1972 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -78,10 +78,9 @@ dependencies { implementation group: "com.github.docker-java", name: "docker-java-transport-httpclient5", version: java_docker_version // TODO: should probably be independent version in future // GIS - // TODO: remove esri library - implementation group: "com.esri.geometry", name: "esri-geometry-api", version: esri_geometry_api_version // Apache 2.0 implementation group: "org.locationtech.jts", name: "jts-core", version: jts_version // Eclipse Public License 2.0 && Eclipse Distribution License 1.0 (BSD-3 Clause) implementation group: "org.locationtech.proj4j", name: "proj4j", version: proj4j_version // Apache 2.0 + implementation group: "org.locationtech.proj4j", name: "proj4j-epsg", version: proj4j_version // Apache 2.0 // uses config internally... /*implementation(group: "com.datastax.oss", name: "java-driver-core", version: cassandra_driver_core_version) { // Apache 2.0 diff --git a/core/src/main/java/org/polypheny/db/algebra/constant/Kind.java b/core/src/main/java/org/polypheny/db/algebra/constant/Kind.java index 64a97ce7fa..58d87bec21 100644 --- a/core/src/main/java/org/polypheny/db/algebra/constant/Kind.java +++ b/core/src/main/java/org/polypheny/db/algebra/constant/Kind.java @@ -16,8 +16,6 @@ package org.polypheny.db.algebra.constant; - -import com.esri.core.geometry.Operator; import java.util.Collection; import java.util.EnumSet; import java.util.Locale; @@ -69,7 +67,7 @@ * kind, and for these we use the {@code SqlOperator}. But for really the common ones, e.g. the many places where we are * looking for {@code AND}, {@code OR} and {@code EQUALS}, the enum helps. * - * (If we were using Scala, {@link Operator} would be a case class, and we wouldn't need {@code Kind}. But we're not.) + * (If we were using Scala, {@link org.polypheny.db.nodes.Operator} would be a case class, and we wouldn't need {@code Kind}. But we're not.) */ public enum Kind { diff --git a/core/src/main/java/org/polypheny/db/algebra/enumerable/RexImpTable.java b/core/src/main/java/org/polypheny/db/algebra/enumerable/RexImpTable.java index 2caf4eb949..e6c94821ae 100644 --- a/core/src/main/java/org/polypheny/db/algebra/enumerable/RexImpTable.java +++ b/core/src/main/java/org/polypheny/db/algebra/enumerable/RexImpTable.java @@ -369,6 +369,8 @@ public Expression implement( RexToLixTranslator translator, RexCall call, ListST_AsText operator function: output the WKT representation of {@link org.polypheny.db.type.entity.spatial.PolyGeometry} + */ + ST_ASTEXT( Function.class ), + + /** + * The ST_Transform operator function: transform coordinates of {@link org.polypheny.db.type.entity.spatial.PolyGeometry} to another SRID + */ + ST_TRANSFORM( Function.class ), + // Common properties /** diff --git a/core/src/main/java/org/polypheny/db/functions/GeoFunctions.java b/core/src/main/java/org/polypheny/db/functions/GeoFunctions.java index 40209bbcbb..4b3d26c97b 100644 --- a/core/src/main/java/org/polypheny/db/functions/GeoFunctions.java +++ b/core/src/main/java/org/polypheny/db/functions/GeoFunctions.java @@ -19,6 +19,7 @@ import static org.polypheny.db.functions.Functions.toUnchecked; import java.util.Objects; +import org.polypheny.db.functions.spatial.GeoTransformFunctions; import org.polypheny.db.type.entity.PolyBoolean; import org.polypheny.db.type.entity.PolyFloat; import org.polypheny.db.type.entity.PolyInteger; @@ -69,11 +70,20 @@ public static PolyGeometry stGeoFromText( PolyString wkt, PolyNumber srid ) { } } - // TODO: transform to another SRID + + @SuppressWarnings("UnusedDeclaration") + public static PolyString stAsText( PolyGeometry geometry ) { + return PolyString.of( geometry.toString() ); + } + + @SuppressWarnings("UnusedDeclaration") public static PolyGeometry stTransform( PolyGeometry geometry, PolyNumber srid ) { - // todo: - return null; + try { + return GeoTransformFunctions.transform( geometry, srid.intValue() ); + } catch ( InvalidGeometryException e ) { + throw toUnchecked( e ); + } } @@ -81,6 +91,7 @@ public static PolyGeometry stTransform( PolyGeometry geometry, PolyNumber srid ) * Common properties */ + @SuppressWarnings("UnusedDeclaration") public static PolyBoolean stIsSimple( PolyGeometry geometry ) { return PolyBoolean.of( geometry.isSimple() ); @@ -179,6 +190,7 @@ public static PolyGeometry stBuffer( PolyGeometry geometry, PolyNumber distance, * Yield metric values */ + @SuppressWarnings("UnusedDeclaration") public static PolyNumber stDistance( PolyGeometry g1, PolyGeometry g2 ) { restrictToSrid( g1, g2 ); diff --git a/core/src/main/java/org/polypheny/db/functions/GeoDistanceFunctions.java b/core/src/main/java/org/polypheny/db/functions/spatial/GeoDistanceFunctions.java similarity index 99% rename from core/src/main/java/org/polypheny/db/functions/GeoDistanceFunctions.java rename to core/src/main/java/org/polypheny/db/functions/spatial/GeoDistanceFunctions.java index 05e6478ac0..310c4736a1 100644 --- a/core/src/main/java/org/polypheny/db/functions/GeoDistanceFunctions.java +++ b/core/src/main/java/org/polypheny/db/functions/spatial/GeoDistanceFunctions.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.polypheny.db.functions; +package org.polypheny.db.functions.spatial; import org.jetbrains.annotations.NotNull; import org.locationtech.jts.geom.Coordinate; diff --git a/core/src/main/java/org/polypheny/db/functions/spatial/GeoTransformFunctions.java b/core/src/main/java/org/polypheny/db/functions/spatial/GeoTransformFunctions.java new file mode 100644 index 0000000000..46d3fffc99 --- /dev/null +++ b/core/src/main/java/org/polypheny/db/functions/spatial/GeoTransformFunctions.java @@ -0,0 +1,155 @@ +/* + * Copyright 2019-2023 The Polypheny Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polypheny.db.functions.spatial; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.proj4j.CRSFactory; +import org.locationtech.proj4j.CoordinateReferenceSystem; +import org.locationtech.proj4j.CoordinateTransform; +import org.locationtech.proj4j.CoordinateTransformFactory; +import org.locationtech.proj4j.Proj4jException; +import org.locationtech.proj4j.ProjCoordinate; +import org.polypheny.db.type.entity.spatial.InvalidGeometryException; +import org.polypheny.db.type.entity.spatial.PolyGeometry; +import org.polypheny.db.type.entity.spatial.PolyGeometryCollection; + +/** + * Transform coordinates to various SRID + */ +public class GeoTransformFunctions { + + private static final String SRID_PREFIX = "EPSG:"; + + + private GeoTransformFunctions() { + // empty on purpose + } + + + public static PolyGeometry transform( PolyGeometry geometry, int srid ) throws InvalidGeometryException { + // Create a CRSFactory to manage coordinate reference systems + CRSFactory crsFactory = new CRSFactory(); + // Create the original and target coordinate reference systems + CoordinateReferenceSystem sourceCrs = crsFactory.createFromName( SRID_PREFIX + geometry.getSRID() ); + CoordinateReferenceSystem targetCrs = crsFactory.createFromName( SRID_PREFIX + srid ); + // create Geometry factory with new srid + GeometryFactory geometryFactory = new GeometryFactory( new PrecisionModel( PrecisionModel.FLOATING ), srid ); + + if ( geometry.isGeometryCollection() ) { + PolyGeometryCollection geometryCollection = geometry.asGeometryCollection(); + Geometry[] geometries = new Geometry[geometryCollection.getNumGeometries()]; + for ( int i = 0; i < geometryCollection.getNumGeometries(); i++ ) { + Geometry geom = geometryCollection.getGeometryN( i ).getJtsGeometry(); + // Convert the Geometry to Proj4J coordinates + ProjCoordinate[] originalCoords = convertToProj4JCoordinates( geom ); + // Perform the SRID conversion + ProjCoordinate[] targetCoords = convertCoordinates( originalCoords, sourceCrs, targetCrs ); + // Convert Proj4J coordinates back to Geometry + geometries[i] = convertToGeometry( targetCoords, geom, geometryFactory ); + } + switch ( geometry.getGeometryType() ) { + case GEOMETRYCOLLECTION: + return PolyGeometry.of( geometryFactory.createGeometryCollection( geometries ) ); + case MULTIPOINT: + Point[] points = new Point[geometries.length]; + for ( int i = 0; i < geometries.length; i++ ) { + points[i] = geometryFactory.createPoint( geometries[i].getCoordinate() ); + } + return PolyGeometry.of( geometryFactory.createMultiPoint( points ) ); + case MULTILINESTRING: + LineString[] lineStrings = new LineString[geometries.length]; + for ( int i = 0; i < geometries.length; i++ ) { + lineStrings[i] = geometryFactory.createLineString( geometries[i].getCoordinates() ); + } + return PolyGeometry.of( geometryFactory.createMultiLineString( lineStrings ) ); + case MULTIPOLYGON: + Polygon[] polygons = new Polygon[geometries.length]; + for ( int i = 0; i < geometries.length; i++ ) { + polygons[i] = geometryFactory.createPolygon( geometries[i].getCoordinates() ); + } + return PolyGeometry.of( geometryFactory.createMultiPolygon( polygons ) ); + default: + throw new InvalidGeometryException( "Cannot convert back to GeometryCollection" ); + } + } else { + ProjCoordinate[] originalCoords = convertToProj4JCoordinates( geometry.getJtsGeometry() ); + ProjCoordinate[] targetCoords = convertCoordinates( originalCoords, sourceCrs, targetCrs ); + return PolyGeometry.of( convertToGeometry( targetCoords, geometry.getJtsGeometry(), geometryFactory ) ); + } + + + } + + + // Convert Geometry coordinates to Proj4J coordinates + private static ProjCoordinate[] convertToProj4JCoordinates( Geometry geometry ) { + ProjCoordinate[] projCoords = new ProjCoordinate[geometry.getNumPoints()]; + for ( int i = 0; i < geometry.getNumPoints(); i++ ) { + projCoords[i] = new ProjCoordinate( geometry.getCoordinates()[i].getX(), geometry.getCoordinates()[i].getY() ); + } + return projCoords; + } + + + // Convert Proj4J coordinates back to Geometry + private static Geometry convertToGeometry( ProjCoordinate[] projCoords, Geometry geometry, GeometryFactory geometryFactory ) throws InvalidGeometryException { + Coordinate[] originalCoordinates = geometry.getCoordinates(); + Coordinate[] coordinates = new Coordinate[projCoords.length]; + + for ( int i = 0; i < projCoords.length; i++ ) { + // X and Y are converted, Z is original + coordinates[i] = new Coordinate( projCoords[i].x, projCoords[i].y, originalCoordinates[i].getZ() ); + } + switch ( geometry.getGeometryType() ) { + case Geometry.TYPENAME_POINT: + return geometryFactory.createPoint( coordinates[0] ); + case Geometry.TYPENAME_LINESTRING: + return geometryFactory.createLineString( coordinates ); + case Geometry.TYPENAME_LINEARRING: + return geometryFactory.createLinearRing( coordinates ); + case Geometry.TYPENAME_POLYGON: + return geometryFactory.createPolygon( coordinates ); + default: + throw new InvalidGeometryException( "Cannot convert back to Geometry" ); + } + + } + + + // Helper method to perform the SRID conversion + private static ProjCoordinate[] convertCoordinates( + ProjCoordinate[] originalCoords, CoordinateReferenceSystem sourceCrs, CoordinateReferenceSystem targetCrs ) { + try { + CoordinateTransform trans = new CoordinateTransformFactory().createTransform( sourceCrs, targetCrs ); + ProjCoordinate[] targetCoords = new ProjCoordinate[originalCoords.length]; + for ( int i = 0; i < originalCoords.length; i++ ) { + targetCoords[i] = new ProjCoordinate(); + trans.transform( originalCoords[i], targetCoords[i] ); + } + return targetCoords; + } catch ( Proj4jException e ) { + throw new RuntimeException( "Error in coordinate transformation.", e ); + } + } + +} diff --git a/core/src/main/java/org/polypheny/db/type/entity/spatial/PolyGeometry.java b/core/src/main/java/org/polypheny/db/type/entity/spatial/PolyGeometry.java index 12969b1057..718a177b6b 100644 --- a/core/src/main/java/org/polypheny/db/type/entity/spatial/PolyGeometry.java +++ b/core/src/main/java/org/polypheny/db/type/entity/spatial/PolyGeometry.java @@ -32,13 +32,11 @@ import org.apache.commons.lang3.NotImplementedException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Coordinates; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.TopologyException; import org.locationtech.jts.io.ParseException; import org.locationtech.jts.io.WKTReader; -import org.polypheny.db.functions.GeoDistanceFunctions; +import org.polypheny.db.functions.spatial.GeoDistanceFunctions; import org.polypheny.db.type.PolySerializable; import org.polypheny.db.type.PolyType; import org.polypheny.db.type.entity.PolyValue; diff --git a/core/src/main/java/org/polypheny/db/type/entity/spatial/PolyPoint.java b/core/src/main/java/org/polypheny/db/type/entity/spatial/PolyPoint.java index 5b49a4a751..127121fa33 100644 --- a/core/src/main/java/org/polypheny/db/type/entity/spatial/PolyPoint.java +++ b/core/src/main/java/org/polypheny/db/type/entity/spatial/PolyPoint.java @@ -27,7 +27,7 @@ *

* The {@link PolyPoint} is valid if * X and Y coordinates are provided. - * The {@link PolyPoint} could store up to 4 dimensions + * The {@link PolyPoint} could store up to 3 dimensions */ public class PolyPoint extends PolyGeometry { @@ -87,14 +87,4 @@ public double getZ() { return jtsPoint.getCoordinate().getZ(); } - - public boolean hasM() { - return !Double.isNaN( jtsPoint.getCoordinate().getM() ); - } - - - public double getM() { - return jtsPoint.getCoordinate().getM(); - } - } diff --git a/core/src/main/java/org/polypheny/db/util/BuiltInMethod.java b/core/src/main/java/org/polypheny/db/util/BuiltInMethod.java index 2a3a87ca7a..4773c09a07 100644 --- a/core/src/main/java/org/polypheny/db/util/BuiltInMethod.java +++ b/core/src/main/java/org/polypheny/db/util/BuiltInMethod.java @@ -447,6 +447,8 @@ public enum BuiltInMethod { UNWRAP_INTERVAL( RefactorFunctions.class, "unwrap", PolyInterval.class ), // GEO METHODS ST_GEOFROMTEXT( GeoFunctions.class, "stGeoFromText", PolyString.class ), + ST_ASTEXT( GeoFunctions.class, "stAsText", PolyGeometry.class ), + ST_TRANSFORM( GeoFunctions.class, "stTransform", PolyGeometry.class, PolyNumber.class ), // Common properties ST_ISSIMPLE( GeoFunctions.class, "stIsSimple", PolyGeometry.class ), ST_ISEMPTY( GeoFunctions.class, "stIsEmpty", PolyGeometry.class ), diff --git a/dbms/src/test/java/org/polypheny/db/sql/fun/GeoFunctionsTest.java b/dbms/src/test/java/org/polypheny/db/sql/fun/GeoFunctionsTest.java index 2cf0112d1d..68be6ff3db 100644 --- a/dbms/src/test/java/org/polypheny/db/sql/fun/GeoFunctionsTest.java +++ b/dbms/src/test/java/org/polypheny/db/sql/fun/GeoFunctionsTest.java @@ -126,6 +126,46 @@ public void commonPropertiesFunctions() throws SQLException { } } + @Test + public void transformFunctions() throws SQLException { + try ( TestHelper.JdbcConnection polyphenyDbConnection = new TestHelper.JdbcConnection( true ) ) { + Connection connection = polyphenyDbConnection.getConnection(); + try ( Statement statement = connection.createStatement() ) { + // transform single point to other SRID + TestHelper.checkResultSet( + statement.executeQuery( "SELECT ST_Transform(ST_GeoFromText('POINT (7.852923 47.998949)', 4326), 2056)" ), + ImmutableList.of( + new Object[]{ "SRID=2056;POINT (2630923.876654428 1316590.5631470187)" } + ) ); + // transform linestring to other SRID + TestHelper.checkResultSet( + statement.executeQuery( "SELECT ST_Transform(ST_GeoFromText('LINESTRING (9.289382 48.741588, 10.289382 47.741588, 12.289382 45.741588)', 4326), 2056)" ), + ImmutableList.of( + new Object[]{ "SRID=2056;LINESTRING (2736179.275459154 1400721.6498003295, 2813774.868277359 1291774.167120458, 2977340.286067275 1077215.140782387)" } + ) ); + // transform polygon to other SRID + TestHelper.checkResultSet( + statement.executeQuery( "SELECT ST_Transform(ST_GeoFromText('POLYGON ((9.289382 48.741588, 10.289382 47.741588, 9.289382 47.741588, 9.289382 48.741588))', 4326), 2056)" ), + ImmutableList.of( + new Object[]{ "SRID=2056;POLYGON ((2736179.275459154 1400721.6498003295, 2813774.868277359 1291774.167120458, 2738803.7053598273 1289526.6432951635, 2736179.275459154 1400721.6498003295))" } + ) ); + // transform geometry collection to other SRID + TestHelper.checkResultSet( + statement.executeQuery( "SELECT ST_Transform(ST_GeoFromText('GEOMETRYCOLLECTION ( POINT(7.852923 47.998949), POINT(9.289382 48.741588))', 4326), 2056)" ), + ImmutableList.of( + new Object[]{ "SRID=2056;GEOMETRYCOLLECTION (POINT (2630923.876654428 1316590.5631470187), POINT (2736179.275459154 1400721.6498003295))" } + ) ); + // transform multipoint to other SRID + TestHelper.checkResultSet( + statement.executeQuery( "SELECT ST_Transform(ST_GeoFromText('MULTIPOINT ((7.852923 47.998949), (9.289382 48.741588))', 4326), 2056)" ), + ImmutableList.of( + new Object[]{ "SRID=2056;MULTIPOINT ((2630923.876654428 1316590.5631470187), (2736179.275459154 1400721.6498003295))" } + ) ); + } + } + + } + @Test public void distanceFunctions() throws SQLException { try ( TestHelper.JdbcConnection polyphenyDbConnection = new TestHelper.JdbcConnection( true ) ) { @@ -144,7 +184,7 @@ public void distanceFunctions() throws SQLException { new Object[]{ 134.45105 } // still the same closest point ) ); // calculate the distance between point and polygon - TestHelper.checkResultSet( // -1 -1, 2 2, -1 2, -1 -1 + TestHelper.checkResultSet( statement.executeQuery( "SELECT ST_Distance(ST_GeoFromText('POINT (7.852923 47.998949)', 4326), ST_GeoFromText('POLYGON ((9.289382 48.741588, 10.289382 47.741588, 9.289382 47.741588, 9.289382 48.741588))', 4326))" ), ImmutableList.of( new Object[]{ 106.87882 } diff --git a/dbms/src/test/java/org/polypheny/db/type/spatial/GeometryTest.java b/dbms/src/test/java/org/polypheny/db/type/spatial/GeometryTest.java index 2a31281f32..86a3a1278a 100644 --- a/dbms/src/test/java/org/polypheny/db/type/spatial/GeometryTest.java +++ b/dbms/src/test/java/org/polypheny/db/type/spatial/GeometryTest.java @@ -84,7 +84,6 @@ public void testPointValidity() { assertEquals( 13.4050, point.getX(), GeometryConstants.DELTA ); assertEquals( 52.5200, point.getY(), GeometryConstants.DELTA ); assertFalse( point.hasZ() ); - assertFalse( point.hasM() ); } ); assertAll( "Group assertions of valid Point in WKT", @@ -97,7 +96,6 @@ public void testPointValidity() { assertEquals( 52.5200, point.getY(), GeometryConstants.DELTA ); assertTrue( point.hasZ() ); assertEquals( 36.754, point.getZ(), GeometryConstants.DELTA ); - assertFalse( point.hasM() ); } ); assertThrows( InvalidGeometryException.class, () -> PolyGeometry.of( "POINT (13.4050)" ) ); diff --git a/plugins/sql-language/src/main/java/org/polypheny/db/sql/SqlLanguagePlugin.java b/plugins/sql-language/src/main/java/org/polypheny/db/sql/SqlLanguagePlugin.java index 5496a70bb0..09c764744f 100644 --- a/plugins/sql-language/src/main/java/org/polypheny/db/sql/SqlLanguagePlugin.java +++ b/plugins/sql-language/src/main/java/org/polypheny/db/sql/SqlLanguagePlugin.java @@ -2490,6 +2490,26 @@ public void unparse( SqlWriter writer, SqlCall call, int leftPrec, int rightPrec // GEO functions register( OperatorName.ST_GEOFROMTEXT, new SqlStGeoFromText() ); + register( + OperatorName.ST_ASTEXT, + new SqlFunction( + "ST_ASTEXT", + Kind.GEO, + ReturnTypes.VARCHAR_2000, + InferTypes.GEOMETRY, + OperandTypes.GEOMETRY, + FunctionCategory.GEOMETRY ) ); + + register( + OperatorName.ST_TRANSFORM, + new SqlFunction( + "ST_TRANSFORM", + Kind.GEO, + ReturnTypes.GEOMETRY, + InferTypes.GEOMETRY, + OperandTypes.GEOMETRY_INTEGER, + FunctionCategory.GEOMETRY ) ); + // Common properties register( OperatorName.ST_ISSIMPLE,