Skip to content

Commit c272c73

Browse files
christophstroblmp911de
authored andcommitted
Fix rewrite near & nearSphere count queries using geoJson to geoWithin.
$near and $nearSphere queries are not supported via countDocuments and the used aggregation match stage and need to be rewritten to $geoWithin. The existing logic did not cover usage of geoJson types, which is fixed now. In case of nearSphere it is also required to convert the $maxDistance argument (given in meters for geoJson) to radians which is used by $geoWithin $centerSphere. Closes #4004 Original pull request: #4006. Related to #2925
1 parent d7fc605 commit c272c73

File tree

4 files changed

+103
-6
lines changed

4 files changed

+103
-6
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java

+31-6
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
import java.util.Map;
2424

2525
import org.bson.Document;
26-
2726
import org.springframework.data.geo.Point;
27+
import org.springframework.data.mongodb.core.query.MetricConversion;
2828
import org.springframework.lang.Nullable;
2929
import org.springframework.util.ObjectUtils;
3030

@@ -162,7 +162,8 @@ private static Document createGeoWithin(String key, Document source, @Nullable O
162162
boolean spheric = source.containsKey("$nearSphere");
163163
Object $near = spheric ? source.get("$nearSphere") : source.get("$near");
164164

165-
Number maxDistance = source.containsKey("$maxDistance") ? (Number) source.get("$maxDistance") : Double.MAX_VALUE;
165+
Number maxDistance = getMaxDistance(source, $near, spheric);
166+
166167
List<Object> $centerMax = Arrays.asList(toCenterCoordinates($near), maxDistance);
167168
Document $geoWithinMax = new Document("$geoWithin",
168169
new Document(spheric ? "$centerSphere" : "$center", $centerMax));
@@ -197,6 +198,24 @@ private static Document createGeoWithin(String key, Document source, @Nullable O
197198
return new Document("$and", criteria);
198199
}
199200

201+
private static Number getMaxDistance(Document source, Object $near, boolean spheric) {
202+
203+
Number maxDistance = Double.MAX_VALUE;
204+
if(source.containsKey("$maxDistance")) { // legacy coordinate pair
205+
maxDistance = (Number) source.get("$maxDistance");
206+
} else if ($near instanceof Document) {
207+
Document nearDoc = (Document)$near;
208+
if(nearDoc.containsKey("$maxDistance")) {
209+
maxDistance = (Number) nearDoc.get("$maxDistance");
210+
// geojson is in Meters but we need radians x/(6378.1*1000)
211+
if(spheric && nearDoc.containsKey("$geometry")) {
212+
maxDistance = MetricConversion.metersToRadians(maxDistance.doubleValue());
213+
}
214+
}
215+
}
216+
return maxDistance;
217+
}
218+
200219
private static boolean containsNear(Document source) {
201220
return source.containsKey("$near") || source.containsKey("$nearSphere");
202221
}
@@ -220,10 +239,16 @@ private static Object toCenterCoordinates(Object value) {
220239
return Arrays.asList(((Point) value).getX(), ((Point) value).getY());
221240
}
222241

223-
if (value instanceof Document && ((Document) value).containsKey("x")) {
224-
225-
Document point = (Document) value;
226-
return Arrays.asList(point.get("x"), point.get("y"));
242+
if (value instanceof Document ) {
243+
Document document = (Document) value;
244+
if(document.containsKey("x")) {
245+
Document point = document;
246+
return Arrays.asList(point.get("x"), point.get("y"));
247+
}
248+
else if (document.containsKey("$geometry")) {
249+
Document geoJsonPoint = document.get("$geometry", Document.class);
250+
return geoJsonPoint.get("coordinates");
251+
}
227252
}
228253

229254
return value;

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java

+24
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.data.mongodb.core.query;
1818

1919
import java.math.BigDecimal;
20+
import java.math.MathContext;
2021
import java.math.RoundingMode;
2122

2223
import org.springframework.data.geo.Distance;
@@ -27,6 +28,7 @@
2728
* {@link Metric} and {@link Distance} conversions using the metric system.
2829
*
2930
* @author Mark Paluch
31+
* @author Christoph Strobl
3032
* @since 2.2
3133
*/
3234
public class MetricConversion {
@@ -61,6 +63,28 @@ public static double getDistanceInMeters(Distance distance) {
6163
.doubleValue();
6264
}
6365

66+
/**
67+
* Return {@code distance} in radians (on an earth like sphere).
68+
*
69+
* @param distance must not be {@literal null}.
70+
* @return distance in rads.
71+
* @since 3.4
72+
*/
73+
public static double toRadians(Distance distance) {
74+
return metersToRadians(getDistanceInMeters(distance));
75+
}
76+
77+
/**
78+
* Return {@code distance} in radians (on an earth like sphere).
79+
*
80+
* @param meters
81+
* @return distance in rads.
82+
* @since 3.4
83+
*/
84+
public static double metersToRadians(double meters) {
85+
return BigDecimal.valueOf(meters).divide(METERS_MULTIPLIER, MathContext.DECIMAL64).doubleValue();
86+
}
87+
6488
/**
6589
* Return {@code metric} to meters multiplier.
6690
*

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CountQueryUnitTests.java

+34
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
3030
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
3131
import org.springframework.data.mongodb.core.convert.QueryMapper;
32+
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
3233
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
3334
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
3435
import org.springframework.data.mongodb.core.query.Criteria;
@@ -155,6 +156,39 @@ void nearToGeoWithinWithMaxDistanceOrCombinedWithOtherCriteria() {
155156
"{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}} ]}"));
156157
}
157158

159+
@Test // GH-4004
160+
void nearToGeoWithinWithMaxDistanceUsingGeoJsonSource() {
161+
162+
Query source = query(new Criteria().orOperator(where("name").is("food"),
163+
where("location").near(new GeoJsonPoint(-73.99171, 40.738868)).maxDistance(10)));
164+
165+
org.bson.Document target = postProcessQueryForCount(source);
166+
assertThat(target).isEqualTo(org.bson.Document.parse(
167+
"{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$center\": [[-73.99171, 40.738868], 10.0]}}} ]}"));
168+
}
169+
170+
@Test // GH-4004
171+
void nearSphereToGeoWithinWithoutMaxDistanceUsingGeoJsonSource() {
172+
173+
Query source = query(new Criteria().orOperator(where("name").is("food"),
174+
where("location").nearSphere(new GeoJsonPoint(-73.99171, 40.738868))));
175+
176+
org.bson.Document target = postProcessQueryForCount(source);
177+
assertThat(target).isEqualTo(org.bson.Document.parse(
178+
"{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 1.7976931348623157E308]}}} ]}"));
179+
}
180+
181+
@Test // GH-4004
182+
void nearSphereToGeoWithinWithMaxDistanceUsingGeoJsonSource() {
183+
184+
Query source = query(new Criteria().orOperator(where("name").is("food"), where("location")
185+
.nearSphere(new GeoJsonPoint(-73.99171, 40.738868)).maxDistance/*in meters for geojson*/(10d)));
186+
187+
org.bson.Document target = postProcessQueryForCount(source);
188+
assertThat(target).isEqualTo(org.bson.Document.parse(
189+
"{\"$or\" : [ { \"name\": \"food\" }, {\"location\": {\"$geoWithin\": {\"$centerSphere\": [[-73.99171, 40.738868], 1.567855942887398E-6]}}} ]}"));
190+
}
191+
158192
private org.bson.Document postProcessQueryForCount(Query source) {
159193

160194
org.bson.Document intermediate = mapper.getMappedObject(source.getQueryObject(), (MongoPersistentEntity<?>) null);

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java

+14
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,18 @@ public void shouldCalculateMetersToMilesMultiplier() {
6565
assertThat(multiplier).isCloseTo(0.00062137, offset(0.000000001));
6666
}
6767

68+
@Test // GH-4004
69+
void shouldConvertMetersToRadians/* on an earth like sphere with r=6378.137km */() {
70+
assertThat(MetricConversion.metersToRadians(1000)).isCloseTo(0.000156785594d, offset(0.000000001));
71+
}
72+
73+
@Test // GH-4004
74+
void shouldConvertKilometersToRadians/* on an earth like sphere with r=6378.137km */() {
75+
assertThat(MetricConversion.toRadians(new Distance(1, Metrics.KILOMETERS))).isCloseTo(0.000156785594d, offset(0.000000001));
76+
}
77+
78+
@Test // GH-4004
79+
void shouldConvertMilesToRadians/* on an earth like sphere with r=6378.137km */() {
80+
assertThat(MetricConversion.toRadians(new Distance(1, Metrics.MILES))).isCloseTo(0.000252321328d, offset(0.000000001));
81+
}
6882
}

0 commit comments

Comments
 (0)