From 3f73552b7e4cd2bd62b51edb3c8042c332b9065e Mon Sep 17 00:00:00 2001 From: Jakub Amanowicz Date: Thu, 7 Nov 2024 10:05:50 +0100 Subject: [PATCH 1/3] CASL-557 buffer transformation Signed-off-by: Jakub Amanowicz --- .../executors/query/WhereClauseBuilder.kt | 23 +++++++++++--- .../naksha/psql/ReadFeaturesByGeometryTest.kt | 31 ++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt index 64817948f..8f918fd57 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt @@ -2,6 +2,7 @@ package naksha.psql.executors.query import naksha.geo.HereTile import naksha.model.* +import naksha.model.GeoEncoding.GeoEncoding_C.TWKB import naksha.model.request.ReadFeatures import naksha.model.request.query.* import naksha.psql.PgColumn @@ -95,13 +96,17 @@ class WhereClauseBuilder(private val request: ReadFeatures) { ) is SpIntersects -> { - // TODO: Add transformations! val twkb = PgUtil.encodeGeometry( spatial.geometry, - Flags().geoGzipOff().geoEncoding(GeoEncoding.TWKB) + Flags().geoGzipOff().geoEncoding(TWKB) ) - val placeholder = placeholderForArg(twkb, PgType.BYTE_ARRAY) - where.append("ST_Intersects(naksha_geometry(${PgColumn.geo}, ${PgColumn.flags}), ST_GeomFromTWKB($placeholder))") + val twkbPlaceholder = placeholderForArg(twkb, PgType.BYTE_ARRAY) + val twkbAsGeometry = "ST_GeomFromTWKB($twkbPlaceholder)" + val geometryToCompare = when(spatial.transformation) { + null -> twkbAsGeometry + else -> resolveTransformation(spatial.transformation!!, twkbAsGeometry) + } + where.append("ST_Intersects(naksha_geometry(${PgColumn.geo}, ${PgColumn.flags}), $geometryToCompare)") } is SpRefInHereTile -> { @@ -115,6 +120,16 @@ class WhereClauseBuilder(private val request: ReadFeatures) { } } + private fun resolveTransformation(transformation: SpTransformation, geometryStmt: String): String { + return when(transformation){ + is SpBuffer -> "ST_Buffer($geometryStmt, ${transformation.distance})" + else -> throw NakshaException( + NakshaError.UNSUPPORTED_OPERATION, + "This transformation is not yet supported: ${transformation::class.simpleName}" + ) + } + } + private fun whereRefTiles() { val hereTiles = request.query.refTiles .filterNotNull() diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt index 4bf0993b8..fad1960c0 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt @@ -7,10 +7,7 @@ import naksha.model.request.ReadFeatures import naksha.model.request.SuccessResponse import naksha.model.request.Write import naksha.model.request.WriteRequest -import naksha.model.request.query.ISpatialQuery -import naksha.model.request.query.SpIntersects -import naksha.model.request.query.SpOr -import naksha.model.request.query.SpRefInHereTile +import naksha.model.request.query.* import naksha.psql.assertions.AnyObjectFluidAssertions.Companion.assertThatAnyObject import naksha.psql.base.PgTestBase import naksha.psql.util.ProxyFeatureGenerator.generateRandomFeature @@ -150,6 +147,32 @@ class ReadFeaturesByGeometryTest : PgTestBase(NakshaCollection("read_by_geometry assertTrue(featureIds.containsAll(listOf(somePlaceInPrague.id, valencia.id))) } + @Test + fun shouldReadByCorridor(){ + // Given + val feature = generateRandomFeature().apply { + geometry = SpLineString().withCoordinates(LineStringCoord( + PointCoord(longitude = 45.0, latitude = 45.0), + PointCoord(longitude = 45.0, latitude = 46.0), + )) + } + + // And + val point = SpPoint(PointCoord(45.000001, 45.0)) + val buffer = SpBuffer(distance = 100000.0) + val readByCorridor = SpIntersects(point, buffer) + + // When + insertFeature(feature) + + // And + val features = executeSpatialQuery(readByCorridor).features + + // Then: + assertEquals(1, features.size) + assertEquals(feature.id, features[0]!!.id) + } + private fun executeSpatialQuery(spatialQuery: ISpatialQuery): SuccessResponse { return executeRead(ReadFeatures().apply { collectionIds += collection!!.id From 45e068fc544796ea54d1753936154bee9d7c4cad Mon Sep 17 00:00:00 2001 From: Jakub Amanowicz Date: Thu, 7 Nov 2024 13:03:32 +0100 Subject: [PATCH 2/3] CASL-557 geography & buffer style params Signed-off-by: Jakub Amanowicz --- .../executors/query/WhereClauseBuilder.kt | 89 ++++++++++++++++--- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt index 8f918fd57..6c8b27bc6 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt @@ -1,8 +1,10 @@ package naksha.psql.executors.query import naksha.geo.HereTile +import naksha.geo.SpGeometry import naksha.model.* import naksha.model.GeoEncoding.GeoEncoding_C.TWKB +import naksha.model.GeoEncoding.GeoEncoding_C.WKB import naksha.model.request.ReadFeatures import naksha.model.request.query.* import naksha.psql.PgColumn @@ -96,15 +98,15 @@ class WhereClauseBuilder(private val request: ReadFeatures) { ) is SpIntersects -> { - val twkb = PgUtil.encodeGeometry( - spatial.geometry, - Flags().geoGzipOff().geoEncoding(TWKB) - ) - val twkbPlaceholder = placeholderForArg(twkb, PgType.BYTE_ARRAY) - val twkbAsGeometry = "ST_GeomFromTWKB($twkbPlaceholder)" val geometryToCompare = when(spatial.transformation) { - null -> twkbAsGeometry - else -> resolveTransformation(spatial.transformation!!, twkbAsGeometry) + null -> { + val twkbPlaceholder = placeholderForArg( + encodedGeo(spatial.geometry, TWKB), + PgType.BYTE_ARRAY + ) + "ST_GeomFromTWKB($twkbPlaceholder)" + } + else -> resolveTransformation(spatial.transformation!!, spatial.geometry) } where.append("ST_Intersects(naksha_geometry(${PgColumn.geo}, ${PgColumn.flags}), $geometryToCompare)") } @@ -120,9 +122,16 @@ class WhereClauseBuilder(private val request: ReadFeatures) { } } - private fun resolveTransformation(transformation: SpTransformation, geometryStmt: String): String { + private fun encodedGeo(geometry: SpGeometry, geoEncoding: Int): ByteArray? { + return PgUtil.encodeGeometry( + geometry, + Flags().geoGzipOff().geoEncoding(geoEncoding) + ) + } + + private fun resolveTransformation(transformation: SpTransformation, geometry: SpGeometry): String { return when(transformation){ - is SpBuffer -> "ST_Buffer($geometryStmt, ${transformation.distance})" + is SpBuffer -> resolveBuffer(transformation, geometry) else -> throw NakshaException( NakshaError.UNSUPPORTED_OPERATION, "This transformation is not yet supported: ${transformation::class.simpleName}" @@ -130,6 +139,66 @@ class WhereClauseBuilder(private val request: ReadFeatures) { } } + private fun resolveBuffer(buffer: SpBuffer, geometry: SpGeometry): String { + val geo = geoParamForBuffer(buffer, geometry) + val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) + val bufferStyleParams = bufferStyleParams(buffer) + return if(bufferStyleParams != null){ + "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" + } else { + "ST_Buffer($geo, $distancePlaceholder)" + } + } + + private fun geoParamForBuffer(buffer: SpBuffer, geometry: SpGeometry): String { + return if(buffer.geography){ + val wkbPlaceholder = placeholderForArg( + encodedGeo(geometry, WKB), + PgType.BYTE_ARRAY + ) + "ST_GeogFromWKB($wkbPlaceholder)" + } else { + val twkbPlaceholder = placeholderForArg( + encodedGeo(geometry, TWKB), + PgType.BYTE_ARRAY + ) + "ST_GeomFromTWKB($twkbPlaceholder)" + } + } + + private fun bufferStyleParams(buffer: SpBuffer): String? { + val bufferStyleParams = StringBuilder() + if(buffer.quadSegments != null){ + val quadSegPlaceholder = placeholderForArg(buffer.quadSegments, PgType.INT) + bufferStyleParams.append("quad_segs=$quadSegPlaceholder") + } + if(buffer.joinStyle != null){ + val joinStylePlaceholder = placeholderForArg(buffer.joinStyle!!.value, PgType.STRING) + if(bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("join=$joinStylePlaceholder") + } + if(buffer.joinLimit != null){ + val joinLimitPlaceholder = placeholderForArg(buffer.joinLimit, PgType.DOUBLE) + if(bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("mitre_limit=$joinLimitPlaceholder") + } + if(buffer.endCap != null){ + val endCapPlaceholder = placeholderForArg(buffer.endCap!!.value, PgType.STRING) + if(bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("endcap=$endCapPlaceholder") + } + if(buffer.side != null){ + val sidePlaceholder = placeholderForArg(buffer.side!!.value, PgType.STRING) + if(bufferStyleParams.isNotEmpty()) bufferStyleParams.append(" ") + bufferStyleParams.append("side=$sidePlaceholder") + } + return if(bufferStyleParams.isNotEmpty()){ + bufferStyleParams.toString() + } else { + null + } + } + private fun whereRefTiles() { val hereTiles = request.query.refTiles .filterNotNull() From 63ab5e0193f5057b1f1c3940112286068664f97b Mon Sep 17 00:00:00 2001 From: Jakub Amanowicz Date: Fri, 8 Nov 2024 10:48:19 +0100 Subject: [PATCH 3/3] CASL-557 review fixes & tests Signed-off-by: Jakub Amanowicz --- .../executors/query/WhereClauseBuilder.kt | 48 +++++++------------ .../naksha/psql/ReadFeaturesByGeometryTest.kt | 39 +++++++++++++++ 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt index 6c8b27bc6..1b61f27f4 100644 --- a/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt +++ b/here-naksha-lib-psql/src/commonMain/kotlin/naksha/psql/executors/query/WhereClauseBuilder.kt @@ -4,7 +4,6 @@ import naksha.geo.HereTile import naksha.geo.SpGeometry import naksha.model.* import naksha.model.GeoEncoding.GeoEncoding_C.TWKB -import naksha.model.GeoEncoding.GeoEncoding_C.WKB import naksha.model.request.ReadFeatures import naksha.model.request.query.* import naksha.psql.PgColumn @@ -98,15 +97,14 @@ class WhereClauseBuilder(private val request: ReadFeatures) { ) is SpIntersects -> { - val geometryToCompare = when(spatial.transformation) { - null -> { - val twkbPlaceholder = placeholderForArg( - encodedGeo(spatial.geometry, TWKB), - PgType.BYTE_ARRAY - ) - "ST_GeomFromTWKB($twkbPlaceholder)" - } - else -> resolveTransformation(spatial.transformation!!, spatial.geometry) + val encodedTwkbPlaceholder = placeholderForArg( + encodedGeo(spatial.geometry, TWKB), + PgType.BYTE_ARRAY + ) + val geometryFromTwkb = "ST_GeomFromTWKB($encodedTwkbPlaceholder)" + val geometryToCompare = when (val transformation = spatial.transformation) { + null -> geometryFromTwkb + else -> resolveTransformation(transformation, geometryFromTwkb) } where.append("ST_Intersects(naksha_geometry(${PgColumn.geo}, ${PgColumn.flags}), $geometryToCompare)") } @@ -129,9 +127,9 @@ class WhereClauseBuilder(private val request: ReadFeatures) { ) } - private fun resolveTransformation(transformation: SpTransformation, geometry: SpGeometry): String { + private fun resolveTransformation(transformation: SpTransformation, geometryFromTwkb: String): String { return when(transformation){ - is SpBuffer -> resolveBuffer(transformation, geometry) + is SpBuffer -> resolveBuffer(transformation, geometryFromTwkb) else -> throw NakshaException( NakshaError.UNSUPPORTED_OPERATION, "This transformation is not yet supported: ${transformation::class.simpleName}" @@ -139,33 +137,21 @@ class WhereClauseBuilder(private val request: ReadFeatures) { } } - private fun resolveBuffer(buffer: SpBuffer, geometry: SpGeometry): String { - val geo = geoParamForBuffer(buffer, geometry) + private fun resolveBuffer(buffer: SpBuffer, geometryFromTwkb: String): String { + val geo = if (buffer.geography) { + "$geometryFromTwkb::geography" + } else { + geometryFromTwkb + } val distancePlaceholder = placeholderForArg(buffer.distance, PgType.DOUBLE) val bufferStyleParams = bufferStyleParams(buffer) - return if(bufferStyleParams != null){ + return if (bufferStyleParams != null) { "ST_Buffer($geo, $distancePlaceholder, $bufferStyleParams)" } else { "ST_Buffer($geo, $distancePlaceholder)" } } - private fun geoParamForBuffer(buffer: SpBuffer, geometry: SpGeometry): String { - return if(buffer.geography){ - val wkbPlaceholder = placeholderForArg( - encodedGeo(geometry, WKB), - PgType.BYTE_ARRAY - ) - "ST_GeogFromWKB($wkbPlaceholder)" - } else { - val twkbPlaceholder = placeholderForArg( - encodedGeo(geometry, TWKB), - PgType.BYTE_ARRAY - ) - "ST_GeomFromTWKB($twkbPlaceholder)" - } - } - private fun bufferStyleParams(buffer: SpBuffer): String? { val bufferStyleParams = StringBuilder() if(buffer.quadSegments != null){ diff --git a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt index fad1960c0..cbdda76f1 100644 --- a/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt +++ b/here-naksha-lib-psql/src/commonTest/kotlin/naksha/psql/ReadFeaturesByGeometryTest.kt @@ -173,6 +173,45 @@ class ReadFeaturesByGeometryTest : PgTestBase(NakshaCollection("read_by_geometry assertEquals(feature.id, features[0]!!.id) } + @Test + fun shouldDistinguishDifferentBufferModes(){ + /** + * Note: samples & values based on https://postgis.net/workshops/postgis-intro/geography.html + * Actual distance between LAX and NRT: + * - 8833954.76996256 meters (Cartesian) - used by 'geography' + * - 258.146005837336 degrees (SRS) - used by 'geometry' + */ + + // Given + val laxAirportCoord = PointCoord(longitude = -118.4079, latitude = 33.9434) + val nrtAirportCoord = PointCoord(longitude = 139.733, latitude = 35.567) + val laxAirport = generateRandomFeature().apply { + geometry = SpPoint(laxAirportCoord) + } + + // When: + insertFeature(laxAirport) + + // And: + val featuresWithinThreeHundredMetersFromNrt = executeSpatialQuery(SpIntersects( + SpPoint(nrtAirportCoord), + SpBuffer(distance = 300.0, geography = true) + )).features + + // Then: + assertTrue(featuresWithinThreeHundredMetersFromNrt.isEmpty()) + + // When: + val featuresWithinThreeHundredDegreesFromNrt = executeSpatialQuery(SpIntersects( + SpPoint(nrtAirportCoord), + SpBuffer(distance = 300.0, geography = false) + )).features + + // Then: + assertEquals(1, featuresWithinThreeHundredDegreesFromNrt.size) + assertEquals(laxAirport.id, featuresWithinThreeHundredDegreesFromNrt[0]!!.id) + } + private fun executeSpatialQuery(spatialQuery: ISpatialQuery): SuccessResponse { return executeRead(ReadFeatures().apply { collectionIds += collection!!.id