diff --git a/pom.xml b/pom.xml index 0e9257f0f0..72e276876c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 3.4.0-SNAPSHOT + 3.4.0-GH-4004-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index e2704a6753..939f29007a 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 3.4.0-SNAPSHOT + 3.4.0-GH-4004-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index b75f8bf624..1ed5ee7d72 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 3.4.0-SNAPSHOT + 3.4.0-GH-4004-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index ca96626cc9..fa0fecddc6 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 3.4.0-SNAPSHOT + 3.4.0-GH-4004-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java index debfb78e59..5c8ee23e10 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java @@ -23,8 +23,8 @@ import java.util.Map; import org.bson.Document; - import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.query.MetricConversion; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -162,7 +162,8 @@ private static Document createGeoWithin(String key, Document source, @Nullable O boolean spheric = source.containsKey("$nearSphere"); Object $near = spheric ? source.get("$nearSphere") : source.get("$near"); - Number maxDistance = source.containsKey("$maxDistance") ? (Number) source.get("$maxDistance") : Double.MAX_VALUE; + Number maxDistance = getMaxDistance(source, $near, spheric); + List $centerMax = Arrays.asList(toCenterCoordinates($near), maxDistance); Document $geoWithinMax = new Document("$geoWithin", new Document(spheric ? "$centerSphere" : "$center", $centerMax)); @@ -193,6 +194,24 @@ private static Document createGeoWithin(String key, Document source, @Nullable O return new Document("$and", criteria); } + private static Number getMaxDistance(Document source, Object $near, boolean spheric) { + + Number maxDistance = Double.MAX_VALUE; + if(source.containsKey("$maxDistance")) { // legacy coordinate pair + maxDistance = (Number) source.get("$maxDistance"); + } else if ($near instanceof Document) { + Document nearDoc = (Document)$near; + if(nearDoc.containsKey("$maxDistance")) { + maxDistance = (Number) nearDoc.get("$maxDistance"); + // geojson is in Meters but we need radians x/(6378.1*1000) + if(spheric && nearDoc.containsKey("$geometry")) { + maxDistance = MetricConversion.metersToRadians(maxDistance.doubleValue()); + } + } + } + return maxDistance; + } + private static boolean containsNear(Document source) { return source.containsKey("$near") || source.containsKey("$nearSphere"); } @@ -216,10 +235,16 @@ private static Object toCenterCoordinates(Object value) { return Arrays.asList(((Point) value).getX(), ((Point) value).getY()); } - if (value instanceof Document && ((Document) value).containsKey("x")) { - - Document point = (Document) value; - return Arrays.asList(point.get("x"), point.get("y")); + if (value instanceof Document ) { + Document document = (Document) value; + if(document.containsKey("x")) { + Document point = document; + return Arrays.asList(point.get("x"), point.get("y")); + } + else if (document.containsKey("$geometry")) { + Document geoJsonPoint = document.get("$geometry", Document.class); + return geoJsonPoint.get("coordinates"); + } } return value; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index d1ea834d9d..0ea24c534c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -624,9 +624,13 @@ public Criteria intersects(GeoJson geoJson) { } /** - * Creates a geo-spatial criterion using a {@literal $maxDistance} operation, for use with $near + * Creates a geo-spatial criterion using a {@literal $maxDistance} operation, for use with {@literal $near} or + * {@literal $nearSphere}. + * + * NOTE: The unit of measure for distance may depends on the used coordinate representation + * (legacy vs. geoJson) as well as the target operation. * - * @param maxDistance + * @param maxDistance radians or meters * @return this. * @see MongoDB Query operator: * $maxDistance @@ -645,8 +649,11 @@ public Criteria maxDistance(double maxDistance) { /** * Creates a geospatial criterion using a {@literal $minDistance} operation, for use with {@literal $near} or * {@literal $nearSphere}. + * + * NOTE: The unit of measure for distance may depends on the used coordinate representation + * (legacy vs. geoJson) as well as the target operation. * - * @param minDistance + * @param minDistance radians or meters * @return this. * @since 1.7 */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java index 30c43f448e..53cf84e47e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.query; import java.math.BigDecimal; +import java.math.MathContext; import java.math.RoundingMode; import org.springframework.data.geo.Distance; @@ -27,6 +28,7 @@ * {@link Metric} and {@link Distance} conversions using the metric system. * * @author Mark Paluch + * @author Christoph Strobl * @since 2.2 */ public class MetricConversion { @@ -61,6 +63,28 @@ public static double getDistanceInMeters(Distance distance) { .doubleValue(); } + /** + * Return {@code distance} in radians (on an earth like sphere). + * + * @param distance must not be {@literal null}. + * @return distance in rads. + * @since 3.4 + */ + public static double toRadians(Distance distance) { + return metersToRadians(getDistanceInMeters(distance)); + } + + /** + * Return {@code distance} in radians (on an earth like sphere). + * + * @param meters + * @return distance in rads. + * @since 3.4 + */ + public static double metersToRadians(double meters) { + return BigDecimal.valueOf(meters).divide(METERS_MULTIPLIER, MathContext.DECIMAL64).doubleValue(); + } + /** * Return {@code metric} to meters multiplier. * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java index e6cd8abde0..b57a5d5d58 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java @@ -15,20 +15,17 @@ */ package org.springframework.data.mongodb.core; -import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; import static org.springframework.data.mongodb.test.util.Assertions.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.data.geo.Point; -import org.springframework.data.mongodb.MongoDatabaseFactory; -import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.query.Criteria; @@ -155,6 +152,39 @@ void nearToGeoWithinWithMaxDistanceOrCombinedWithOtherCriteria() { "{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}} ]}")); } + @Test // GH-4004 + void nearToGeoWithinWithMaxDistanceUsingGeoJsonSource() { + + Query source = query(new Criteria().orOperator(where("name").is("food"), + where("location").near(new GeoJsonPoint(-73.99171, 40.738868)).maxDistance(10))); + + org.bson.Document target = postProcessQueryForCount(source); + assertThat(target).isEqualTo(org.bson.Document.parse( + "{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}} ]}")); + } + + @Test // GH-4004 + void nearSphereToGeoWithinWithoutMaxDistanceUsingGeoJsonSource() { + + Query source = query(new Criteria().orOperator(where("name").is("food"), + where("location").nearSphere(new GeoJsonPoint(-73.99171, 40.738868)))); + + org.bson.Document target = postProcessQueryForCount(source); + assertThat(target).isEqualTo(org.bson.Document.parse( + "{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 1.7976931348623157E308]}}} ]}")); + } + + @Test // GH-4004 + void nearSphereToGeoWithinWithMaxDistanceUsingGeoJsonSource() { + + Query source = query(new Criteria().orOperator(where("name").is("food"), where("location") + .nearSphere(new GeoJsonPoint(-73.99171, 40.738868)).maxDistance/*in meters for geojson*/(10d))); + + org.bson.Document target = postProcessQueryForCount(source); + assertThat(target).isEqualTo(org.bson.Document.parse( + "{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 1.567855942887398E-6]}}} ]}")); + } + private org.bson.Document postProcessQueryForCount(Query source) { org.bson.Document intermediate = mapper.getMappedObject(source.getQueryObject(), (MongoPersistentEntity>) null); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java index fc8b072f33..9447cfc383 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java @@ -65,4 +65,18 @@ public void shouldCalculateMetersToMilesMultiplier() { assertThat(multiplier).isCloseTo(0.00062137, offset(0.000000001)); } + @Test // GH-4004 + void shouldConvertMetersToRadians/* on an earth like sphere with r=6378.137km */() { + assertThat(MetricConversion.metersToRadians(1000)).isCloseTo(0.000156785594d, offset(0.000000001)); + } + + @Test // GH-4004 + void shouldConvertKilometersToRadians/* on an earth like sphere with r=6378.137km */() { + assertThat(MetricConversion.toRadians(new Distance(1, Metrics.KILOMETERS))).isCloseTo(0.000156785594d, offset(0.000000001)); + } + + @Test // GH-4004 + void shouldConvertMilesToRadians/* on an earth like sphere with r=6378.137km */() { + assertThat(MetricConversion.toRadians(new Distance(1, Metrics.MILES))).isCloseTo(0.000252321328d, offset(0.000000001)); + } }
+ * NOTE: The unit of measure for distance may depends on the used coordinate representation + * (legacy vs. geoJson) as well as the target operation. * - * @param maxDistance + * @param maxDistance radians or meters * @return this. * @see MongoDB Query operator: * $maxDistance @@ -645,8 +649,11 @@ public Criteria maxDistance(double maxDistance) { /** * Creates a geospatial criterion using a {@literal $minDistance} operation, for use with {@literal $near} or * {@literal $nearSphere}. + *
+ * NOTE: The unit of measure for distance may depends on the used coordinate representation + * (legacy vs. geoJson) as well as the target operation. * - * @param minDistance + * @param minDistance radians or meters * @return this. * @since 1.7 */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java index 30c43f448e..53cf84e47e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.query; import java.math.BigDecimal; +import java.math.MathContext; import java.math.RoundingMode; import org.springframework.data.geo.Distance; @@ -27,6 +28,7 @@ * {@link Metric} and {@link Distance} conversions using the metric system. * * @author Mark Paluch + * @author Christoph Strobl * @since 2.2 */ public class MetricConversion { @@ -61,6 +63,28 @@ public static double getDistanceInMeters(Distance distance) { .doubleValue(); } + /** + * Return {@code distance} in radians (on an earth like sphere). + * + * @param distance must not be {@literal null}. + * @return distance in rads. + * @since 3.4 + */ + public static double toRadians(Distance distance) { + return metersToRadians(getDistanceInMeters(distance)); + } + + /** + * Return {@code distance} in radians (on an earth like sphere). + * + * @param meters + * @return distance in rads. + * @since 3.4 + */ + public static double metersToRadians(double meters) { + return BigDecimal.valueOf(meters).divide(METERS_MULTIPLIER, MathContext.DECIMAL64).doubleValue(); + } + /** * Return {@code metric} to meters multiplier. * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java index e6cd8abde0..b57a5d5d58 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java @@ -15,20 +15,17 @@ */ package org.springframework.data.mongodb.core; -import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; import static org.springframework.data.mongodb.test.util.Assertions.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.data.geo.Point; -import org.springframework.data.mongodb.MongoDatabaseFactory; -import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.query.Criteria; @@ -155,6 +152,39 @@ void nearToGeoWithinWithMaxDistanceOrCombinedWithOtherCriteria() { "{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}} ]}")); } + @Test // GH-4004 + void nearToGeoWithinWithMaxDistanceUsingGeoJsonSource() { + + Query source = query(new Criteria().orOperator(where("name").is("food"), + where("location").near(new GeoJsonPoint(-73.99171, 40.738868)).maxDistance(10))); + + org.bson.Document target = postProcessQueryForCount(source); + assertThat(target).isEqualTo(org.bson.Document.parse( + "{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}} ]}")); + } + + @Test // GH-4004 + void nearSphereToGeoWithinWithoutMaxDistanceUsingGeoJsonSource() { + + Query source = query(new Criteria().orOperator(where("name").is("food"), + where("location").nearSphere(new GeoJsonPoint(-73.99171, 40.738868)))); + + org.bson.Document target = postProcessQueryForCount(source); + assertThat(target).isEqualTo(org.bson.Document.parse( + "{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 1.7976931348623157E308]}}} ]}")); + } + + @Test // GH-4004 + void nearSphereToGeoWithinWithMaxDistanceUsingGeoJsonSource() { + + Query source = query(new Criteria().orOperator(where("name").is("food"), where("location") + .nearSphere(new GeoJsonPoint(-73.99171, 40.738868)).maxDistance/*in meters for geojson*/(10d))); + + org.bson.Document target = postProcessQueryForCount(source); + assertThat(target).isEqualTo(org.bson.Document.parse( + "{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 1.567855942887398E-6]}}} ]}")); + } + private org.bson.Document postProcessQueryForCount(Query source) { org.bson.Document intermediate = mapper.getMappedObject(source.getQueryObject(), (MongoPersistentEntity>) null); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java index fc8b072f33..9447cfc383 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java @@ -65,4 +65,18 @@ public void shouldCalculateMetersToMilesMultiplier() { assertThat(multiplier).isCloseTo(0.00062137, offset(0.000000001)); } + @Test // GH-4004 + void shouldConvertMetersToRadians/* on an earth like sphere with r=6378.137km */() { + assertThat(MetricConversion.metersToRadians(1000)).isCloseTo(0.000156785594d, offset(0.000000001)); + } + + @Test // GH-4004 + void shouldConvertKilometersToRadians/* on an earth like sphere with r=6378.137km */() { + assertThat(MetricConversion.toRadians(new Distance(1, Metrics.KILOMETERS))).isCloseTo(0.000156785594d, offset(0.000000001)); + } + + @Test // GH-4004 + void shouldConvertMilesToRadians/* on an earth like sphere with r=6378.137km */() { + assertThat(MetricConversion.toRadians(new Distance(1, Metrics.MILES))).isCloseTo(0.000252321328d, offset(0.000000001)); + } }