Skip to content

Commit

Permalink
Geospatial queries support (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
ulhasrm authored Mar 21, 2020
1 parent 7e2f5db commit 3db5278
Show file tree
Hide file tree
Showing 12 changed files with 531 additions and 45 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ EndingWith
Like
NotLike
Regex
Distinct
IsEmpty
ExistsBy
IsWithin
IsNear
```

# Community
Expand Down
2 changes: 1 addition & 1 deletion checkstyle/checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@
</module>
<module name="InnerAssignment"/>
<module name="ReturnCount">
<property name="max" value="8"/>
<property name="max" value="9"/>
<property name="maxForVoid" value="8"/>
</module>
<module name="NestedIfDepth">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
*
* 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.springframework.data.hazelcast.repository.query;

import static com.hazelcast.query.impl.IndexUtils.canonicalizeAttribute;

import java.util.Map;

import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;

import com.hazelcast.query.Predicate;
import com.hazelcast.query.impl.Extractable;
/**
* Geo Predicate - Used to calculate near and within queries
* <li>Finds all the Points within the given distance range from source Point.
* <li>Finds all the Points within given Circle.
*
* @param <K> key of map entry
* @param <V> value of map entry
* @author Ulhas R Manekar
*/
public class GeoPredicate<K, V>
implements Predicate<K, V> {

private static final double KM_TO_MILES = 0.621371;
private static final double KM_TO_NEUTRAL = 0.539957;
private static final double R = 6372.8;

final String attributeName;
final Point queryPoint;
final Distance distance;

/**
* Constructor accepts the name of the attribute which is of type Point.
* Constructs a new geo predicate on the given point
* @param attribute the name of the attribute in a object within Map which is of type Point.
* @param point the source point from where the distance is calculated.
* @param distance the Distance object with value and unit of distance.
*/
public GeoPredicate(String attribute, Point point, Distance distance) {
this.attributeName = canonicalizeAttribute(attribute);
this.queryPoint = point;
this.distance = distance;
}

@Override
public boolean apply(Map.Entry<K, V> mapEntry) {
Object attributeValue = readAttributeValue(mapEntry);
if (attributeValue instanceof Point) {
return compareDistance((Point) attributeValue);
} else {
throw new IllegalArgumentException(String.format("Cannot use %s predicate with attribute other than Point",
getClass().getSimpleName()));
}
}

private boolean compareDistance(Point point) {
double calculatedDistance = calculateDistance(point.getX(), point.getY(), this.queryPoint.getX(),
this.queryPoint.getY(), this.distance.getMetric());
return calculatedDistance < this.distance.getValue();
}

/**
* This method users Haversine formula to calculate the distance between two points
* Formula is explained here - https://www.movable-type.co.uk/scripts/gis-faq-5.1.html
* Sample Java code is here - https://rosettacode.org/wiki/Haversine_formula#Java
* @param lat1 - Latitude of first point.
* @param lng1 - Longitude of first point.
* @param lat2 - Latitude of second point.
* @param lng2 - Longitude of second point.
* @param metric - metric to specify where its KILOMETERS, MILES or NEUTRAL
* @return
*/
private double calculateDistance(double lat1, double lng1, double lat2, double lng2, Metric metric) {
if ((lat1 == lat2) && (lng1 == lng2)) {
return 0;
} else {
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lng2 - lng1);
double lat1Radians = Math.toRadians(lat1);
double lat2Radians = Math.toRadians(lat2);

double a = Math.pow(Math.sin(dLat / 2), 2)
+ Math.pow(Math.sin(dLon / 2), 2) * Math.cos(lat1Radians) * Math.cos(lat2Radians);
double c = 2 * Math.asin(Math.sqrt(a));
double dist = R * c;

if (Metrics.MILES.equals(metric)) {
dist = dist * KM_TO_MILES;
} else if (Metrics.NEUTRAL.equals(metric)) {
dist = dist * KM_TO_NEUTRAL;
}

return dist;
}
}

private Object readAttributeValue(Map.Entry<K, V> entry) {
Extractable extractable = (Extractable) entry;
return extractable.getAttributeValue(this.attributeName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
import com.hazelcast.query.impl.predicates.PagingPredicateImpl;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.query.ParameterAccessor;
Expand Down Expand Up @@ -179,10 +183,11 @@ public HazelcastQueryCreator(PartTree tree, ParameterAccessor parameters) {
case IS_EMPTY:
case IS_NOT_EMPTY:
return fromEmptyVariant(type, property);
/* case EXISTS:
* case NEAR:
* case WITHIN:
*/
/* case EXISTS:*/
case NEAR:
case WITHIN:
return fromGeoVariant(type, property, iterator);

default:
throw new InvalidDataAccessApiUsageException(String.format("Unsupported type '%s'", type));
}
Expand Down Expand Up @@ -329,4 +334,43 @@ private Comparable<?>[] collectToArray(Type type, Iterator<Comparable<?>> iterat
throw new InvalidDataAccessApiUsageException(String.format("Logic error for '%s' in query", type));
}
}

private Predicate<?, ?> fromGeoVariant(Type type, String property, Iterator<Comparable<?>> iterator) {
final Object item = iterator.next();
Point point;
Distance distance;
if (item instanceof Point) {
point = (Point) item;
if (!iterator.hasNext()) {
throw new InvalidDataAccessApiUsageException(
"Expected to find distance value for geo query. Are you missing a parameter?");
}

Object distObject = iterator.next();
if (distObject instanceof Distance) {
distance = (Distance) distObject;
} else if (distObject instanceof Number) {
distance = new Distance(((Number) distObject).doubleValue(), Metrics.KILOMETERS);
} else {
throw new InvalidDataAccessApiUsageException(String
.format("Expected to find Distance or Numeric value for geo query but was %s.",
distObject.getClass()));
}
} else if (item instanceof Circle) {
point = ((Circle) item).getCenter();
distance = ((Circle) item).getRadius();
} else {
throw new InvalidDataAccessApiUsageException(
String.format("Expected to find a Circle or Point/Distance for geo query but was %s.", item.getClass()));
}

switch (type) {
case WITHIN:
case NEAR:
return new GeoPredicate<>(property, point, distance);

default:
throw new InvalidDataAccessApiUsageException(String.format("Logic error for '%s' in query", type));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.test.context.ActiveProfiles;
import test.utils.Oscars;
import test.utils.TestData;
import test.utils.TestConstants;
import test.utils.TestDataHelper;
import test.utils.domain.Person;
Expand Down Expand Up @@ -73,7 +73,7 @@ public void unpaged() {
assertThat("First page count matches content", page.getNumberOfElements(), equalTo(content.size()));
assertThat("First page has all content", (long) page.getNumberOfElements(), equalTo(page.getTotalElements()));
assertThat("First page has no upper limit", page.getSize(), equalTo(0));
assertThat("First page has correct content count", page.getNumberOfElements(), equalTo(Oscars.bestActors.length));
assertThat("First page has correct content count", page.getNumberOfElements(), equalTo(TestData.bestActors.length));
assertThat("First page is only page", page.getTotalPages(), equalTo(1));
}

Expand Down Expand Up @@ -136,7 +136,7 @@ public void unsorted() {
iterator.next();
}

assertThat("Correct number, order undefined", count, equalTo(Oscars.bestActors.length));
assertThat("Correct number, order undefined", count, equalTo(TestData.bestActors.length));
}

@Test
Expand All @@ -162,7 +162,7 @@ public void sorting() {
count++;
previousFirstname = person.getFirstname();
}
assertThat("Everything found ascending", count, equalTo(Oscars.bestActors.length));
assertThat("Everything found ascending", count, equalTo(TestData.bestActors.length));

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

Expand Down
Loading

0 comments on commit 3db5278

Please sign in to comment.