Skip to content
This repository was archived by the owner on May 28, 2026. It is now read-only.

Commit 3db5278

Browse files
authored
Geospatial queries support (#98)
1 parent 7e2f5db commit 3db5278

12 files changed

Lines changed: 531 additions & 45 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ EndingWith
119119
Like
120120
NotLike
121121
Regex
122+
Distinct
123+
IsEmpty
124+
ExistsBy
125+
IsWithin
126+
IsNear
122127
```
123128

124129
# Community

checkstyle/checkstyle.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@
249249
</module>
250250
<module name="InnerAssignment"/>
251251
<module name="ReturnCount">
252-
<property name="max" value="8"/>
252+
<property name="max" value="9"/>
253253
<property name="maxForVoid" value="8"/>
254254
</module>
255255
<module name="NestedIfDepth">
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.hazelcast.repository.query;
17+
18+
import static com.hazelcast.query.impl.IndexUtils.canonicalizeAttribute;
19+
20+
import java.util.Map;
21+
22+
import org.springframework.data.geo.Distance;
23+
import org.springframework.data.geo.Metric;
24+
import org.springframework.data.geo.Metrics;
25+
import org.springframework.data.geo.Point;
26+
27+
import com.hazelcast.query.Predicate;
28+
import com.hazelcast.query.impl.Extractable;
29+
/**
30+
* Geo Predicate - Used to calculate near and within queries
31+
* <li>Finds all the Points within the given distance range from source Point.
32+
* <li>Finds all the Points within given Circle.
33+
*
34+
* @param <K> key of map entry
35+
* @param <V> value of map entry
36+
* @author Ulhas R Manekar
37+
*/
38+
public class GeoPredicate<K, V>
39+
implements Predicate<K, V> {
40+
41+
private static final double KM_TO_MILES = 0.621371;
42+
private static final double KM_TO_NEUTRAL = 0.539957;
43+
private static final double R = 6372.8;
44+
45+
final String attributeName;
46+
final Point queryPoint;
47+
final Distance distance;
48+
49+
/**
50+
* Constructor accepts the name of the attribute which is of type Point.
51+
* Constructs a new geo predicate on the given point
52+
* @param attribute the name of the attribute in a object within Map which is of type Point.
53+
* @param point the source point from where the distance is calculated.
54+
* @param distance the Distance object with value and unit of distance.
55+
*/
56+
public GeoPredicate(String attribute, Point point, Distance distance) {
57+
this.attributeName = canonicalizeAttribute(attribute);
58+
this.queryPoint = point;
59+
this.distance = distance;
60+
}
61+
62+
@Override
63+
public boolean apply(Map.Entry<K, V> mapEntry) {
64+
Object attributeValue = readAttributeValue(mapEntry);
65+
if (attributeValue instanceof Point) {
66+
return compareDistance((Point) attributeValue);
67+
} else {
68+
throw new IllegalArgumentException(String.format("Cannot use %s predicate with attribute other than Point",
69+
getClass().getSimpleName()));
70+
}
71+
}
72+
73+
private boolean compareDistance(Point point) {
74+
double calculatedDistance = calculateDistance(point.getX(), point.getY(), this.queryPoint.getX(),
75+
this.queryPoint.getY(), this.distance.getMetric());
76+
return calculatedDistance < this.distance.getValue();
77+
}
78+
79+
/**
80+
* This method users Haversine formula to calculate the distance between two points
81+
* Formula is explained here - https://www.movable-type.co.uk/scripts/gis-faq-5.1.html
82+
* Sample Java code is here - https://rosettacode.org/wiki/Haversine_formula#Java
83+
* @param lat1 - Latitude of first point.
84+
* @param lng1 - Longitude of first point.
85+
* @param lat2 - Latitude of second point.
86+
* @param lng2 - Longitude of second point.
87+
* @param metric - metric to specify where its KILOMETERS, MILES or NEUTRAL
88+
* @return
89+
*/
90+
private double calculateDistance(double lat1, double lng1, double lat2, double lng2, Metric metric) {
91+
if ((lat1 == lat2) && (lng1 == lng2)) {
92+
return 0;
93+
} else {
94+
double dLat = Math.toRadians(lat2 - lat1);
95+
double dLon = Math.toRadians(lng2 - lng1);
96+
double lat1Radians = Math.toRadians(lat1);
97+
double lat2Radians = Math.toRadians(lat2);
98+
99+
double a = Math.pow(Math.sin(dLat / 2), 2)
100+
+ Math.pow(Math.sin(dLon / 2), 2) * Math.cos(lat1Radians) * Math.cos(lat2Radians);
101+
double c = 2 * Math.asin(Math.sqrt(a));
102+
double dist = R * c;
103+
104+
if (Metrics.MILES.equals(metric)) {
105+
dist = dist * KM_TO_MILES;
106+
} else if (Metrics.NEUTRAL.equals(metric)) {
107+
dist = dist * KM_TO_NEUTRAL;
108+
}
109+
110+
return dist;
111+
}
112+
}
113+
114+
private Object readAttributeValue(Map.Entry<K, V> entry) {
115+
Extractable extractable = (Extractable) entry;
116+
return extractable.getAttributeValue(this.attributeName);
117+
}
118+
}

src/main/java/org/springframework/data/hazelcast/repository/query/HazelcastQueryCreator.java

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
import com.hazelcast.query.impl.predicates.PagingPredicateImpl;
2121
import org.springframework.dao.InvalidDataAccessApiUsageException;
2222
import org.springframework.data.domain.Sort;
23+
import org.springframework.data.geo.Circle;
24+
import org.springframework.data.geo.Distance;
25+
import org.springframework.data.geo.Metrics;
26+
import org.springframework.data.geo.Point;
2327
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
2428
import org.springframework.data.mapping.PropertyPath;
2529
import org.springframework.data.repository.query.ParameterAccessor;
@@ -179,10 +183,11 @@ public HazelcastQueryCreator(PartTree tree, ParameterAccessor parameters) {
179183
case IS_EMPTY:
180184
case IS_NOT_EMPTY:
181185
return fromEmptyVariant(type, property);
182-
/* case EXISTS:
183-
* case NEAR:
184-
* case WITHIN:
185-
*/
186+
/* case EXISTS:*/
187+
case NEAR:
188+
case WITHIN:
189+
return fromGeoVariant(type, property, iterator);
190+
186191
default:
187192
throw new InvalidDataAccessApiUsageException(String.format("Unsupported type '%s'", type));
188193
}
@@ -329,4 +334,43 @@ private Comparable<?>[] collectToArray(Type type, Iterator<Comparable<?>> iterat
329334
throw new InvalidDataAccessApiUsageException(String.format("Logic error for '%s' in query", type));
330335
}
331336
}
337+
338+
private Predicate<?, ?> fromGeoVariant(Type type, String property, Iterator<Comparable<?>> iterator) {
339+
final Object item = iterator.next();
340+
Point point;
341+
Distance distance;
342+
if (item instanceof Point) {
343+
point = (Point) item;
344+
if (!iterator.hasNext()) {
345+
throw new InvalidDataAccessApiUsageException(
346+
"Expected to find distance value for geo query. Are you missing a parameter?");
347+
}
348+
349+
Object distObject = iterator.next();
350+
if (distObject instanceof Distance) {
351+
distance = (Distance) distObject;
352+
} else if (distObject instanceof Number) {
353+
distance = new Distance(((Number) distObject).doubleValue(), Metrics.KILOMETERS);
354+
} else {
355+
throw new InvalidDataAccessApiUsageException(String
356+
.format("Expected to find Distance or Numeric value for geo query but was %s.",
357+
distObject.getClass()));
358+
}
359+
} else if (item instanceof Circle) {
360+
point = ((Circle) item).getCenter();
361+
distance = ((Circle) item).getRadius();
362+
} else {
363+
throw new InvalidDataAccessApiUsageException(
364+
String.format("Expected to find a Circle or Point/Distance for geo query but was %s.", item.getClass()));
365+
}
366+
367+
switch (type) {
368+
case WITHIN:
369+
case NEAR:
370+
return new GeoPredicate<>(property, point, distance);
371+
372+
default:
373+
throw new InvalidDataAccessApiUsageException(String.format("Logic error for '%s' in query", type));
374+
}
375+
}
332376
}

src/test/java/org/springframework/data/hazelcast/repository/PagingSortingIT.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import org.springframework.data.domain.Sort;
2424
import org.springframework.data.repository.PagingAndSortingRepository;
2525
import org.springframework.test.context.ActiveProfiles;
26-
import test.utils.Oscars;
26+
import test.utils.TestData;
2727
import test.utils.TestConstants;
2828
import test.utils.TestDataHelper;
2929
import test.utils.domain.Person;
@@ -73,7 +73,7 @@ public void unpaged() {
7373
assertThat("First page count matches content", page.getNumberOfElements(), equalTo(content.size()));
7474
assertThat("First page has all content", (long) page.getNumberOfElements(), equalTo(page.getTotalElements()));
7575
assertThat("First page has no upper limit", page.getSize(), equalTo(0));
76-
assertThat("First page has correct content count", page.getNumberOfElements(), equalTo(Oscars.bestActors.length));
76+
assertThat("First page has correct content count", page.getNumberOfElements(), equalTo(TestData.bestActors.length));
7777
assertThat("First page is only page", page.getTotalPages(), equalTo(1));
7878
}
7979

@@ -136,7 +136,7 @@ public void unsorted() {
136136
iterator.next();
137137
}
138138

139-
assertThat("Correct number, order undefined", count, equalTo(Oscars.bestActors.length));
139+
assertThat("Correct number, order undefined", count, equalTo(TestData.bestActors.length));
140140
}
141141

142142
@Test
@@ -162,7 +162,7 @@ public void sorting() {
162162
count++;
163163
previousFirstname = person.getFirstname();
164164
}
165-
assertThat("Everything found ascending", count, equalTo(Oscars.bestActors.length));
165+
assertThat("Everything found ascending", count, equalTo(TestData.bestActors.length));
166166

167167
assertThat("1956 winner, last firstname ascending", previousFirstname, equalTo("Yul"));
168168

0 commit comments

Comments
 (0)