DATACOUCH-165 - Add geospatial support

Add support for geo queries through the use of Spatial Views. This needs the query method to be annotated with @Dimensional (or a meta-annotation of it that is runtime retained).

See SpatialViewQueryCreator javadoc for supported part types and corresponding expected arguments (to be documented).
In a nutshell WITHIN Box (other shapes supported but approximated to bounding box), NEAR Point+Distance (approximated to bounding box), plus a few simple operators for dimensions beyond 2.

When approximating, a DEBUG log is made along the logging of the query.

Conversion methods from Shapes to bounding boxes are done in a GeoUtils class.

Unit and Integration test of the feature.

Added documentation for @Dimensional query derivation.
This commit is contained in:
Simon Baslé
2015-09-22 20:41:02 +02:00
parent 2914f57897
commit a2f26f4310
22 changed files with 1461 additions and 20 deletions

View File

@@ -28,6 +28,8 @@ import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.N1qlQueryResult;
import com.couchbase.client.java.query.N1qlParams;
import com.couchbase.client.java.query.Statement;
import com.couchbase.client.java.view.SpatialViewQuery;
import com.couchbase.client.java.view.SpatialViewResult;
import com.couchbase.client.java.view.ViewQuery;
import com.couchbase.client.java.view.ViewResult;
@@ -213,6 +215,28 @@ public interface CouchbaseOperations {
*/
ViewResult queryView(ViewQuery query);
/**
* Query a Spatial View for a list of documents of type T.
* <p/>
* <p>It is valid to pass in a empty constructed {@link SpatialViewQuery} object.</p>
* <p/>
*
* @param query the SpatialViewQuery object (also specifying view design document and view name).
* @param entityClass the entity to map to.
* @return the converted collection
*/
<T> List<T> findBySpatialView(SpatialViewQuery query, Class<T> entityClass);
/**
* Query a Spatial View with direct access to the {@link SpatialViewResult}.
* <p>This method is available to ease the working with spatial views by still wrapping exceptions into the Spring
* infrastructure.</p>
*
* @param query the SpatialViewQuery object (also specifying view design document and view name).
* @return SpatialViewResult containing the results of the query.
*/
SpatialViewResult querySpatialView(SpatialViewQuery query);
/**
* Query the N1QL Service for JSON data of type T. Enough data to construct the full
* entity is expected to be selected, including the metadata {@value #SELECT_ID} and
@@ -349,5 +373,4 @@ public interface CouchbaseOperations {
* @return the consistency to use for generated repository queries.
*/
Consistency getDefaultConsistency();
}
}

View File

@@ -41,6 +41,9 @@ import com.couchbase.client.java.query.N1qlQueryRow;
import com.couchbase.client.java.query.N1qlParams;
import com.couchbase.client.java.query.consistency.ScanConsistency;
import com.couchbase.client.java.util.features.CouchbaseFeature;
import com.couchbase.client.java.view.SpatialViewQuery;
import com.couchbase.client.java.view.SpatialViewResult;
import com.couchbase.client.java.view.SpatialViewRow;
import com.couchbase.client.java.view.ViewQuery;
import com.couchbase.client.java.view.ViewResult;
import com.couchbase.client.java.view.ViewRow;
@@ -292,7 +295,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
@Override
public <T> List<T> findByView(ViewQuery query, Class<T> entityClass) {
query.includeDocs(false);
query.includeDocs(false); //FIXME the doc says it is set to true...
query.reduce(false);
try {
@@ -326,6 +329,41 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP
});
}
@Override
public <T> List<T> findBySpatialView(SpatialViewQuery query, Class<T> entityClass) {
//note: don't make any assumption about includeDocs and let the user decide
try {
final SpatialViewResult response = querySpatialView(query);
if (response.error() != null) {
throw new CouchbaseQueryExecutionException("Unable to execute spatial view query due to the following view error: " +
response.error().toString());
}
List<SpatialViewRow> allRows = response.allRows();
final List<T> result = new ArrayList<T>(allRows.size());
for (final SpatialViewRow row : allRows) {
result.add(mapToEntity(row.id(), row.document(RawJsonDocument.class), entityClass));
}
return result;
}
catch (TranscodingException e) {
throw new CouchbaseQueryExecutionException("Unable to execute view query", e);
}
}
@Override
public SpatialViewResult querySpatialView(final SpatialViewQuery query) {
return execute(new BucketCallback<SpatialViewResult>() {
@Override
public SpatialViewResult doInBucket() throws TimeoutException, ExecutionException, InterruptedException {
return client.query(query);
}
});
}
@Override
public <T> List<T> findByN1QL(N1qlQuery n1ql, Class<T> entityClass) {
checkN1ql();

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2015 the original author or authors
*
* 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.couchbase.core.view;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.QueryAnnotation;
/**
* An annotation to mark a repository method as querying using a Couchbase Spatial View.
* The attributes will be used to resolve the actual Spatial View to be used and to determine
* the expected number of dimensions in the Spatial View keys. It can also be used as meta-annotation.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@QueryAnnotation
public @interface Dimensional {
/** The name of the design document to be queried */
String designDocument();
/** The name of the spatial view to be queried */
String spatialViewName();
/** The number of dimensions in the spatial view, defaults to 2 */
int dimensions() default 2;
}

View File

@@ -49,6 +49,10 @@ import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
* {@value StringN1qlBasedQuery#PLACEHOLDER_ENTITY}
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_ENTITY})
* </li>
* <li>
* {@value StringN1qlBasedQuery#PLACEHOLDER_FILTER_TYPE}
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_FILTER_TYPE})
* </li>
* </ul>
*
* @author Simon Baslé.

View File

@@ -22,6 +22,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.QueryAnnotation;
/**
* Annotation to support the use of Views with Couchbase.
*
@@ -31,6 +33,7 @@ import java.lang.annotation.Target;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
//don't set @QueryAnnotation, as it causes problems with reduce and replacing count() method, the reduce detection needs to be improved
public @interface View {
/**

View File

@@ -21,6 +21,7 @@ import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.view.Dimensional;
import org.springframework.data.couchbase.core.view.Query;
import org.springframework.data.couchbase.core.view.View;
import org.springframework.data.mapping.context.MappingContext;
@@ -101,6 +102,21 @@ public class CouchbaseQueryMethod extends QueryMethod {
return method.getAnnotation(View.class);
}
/**
* @return true if the method has a @Dimensional annotation, false otherwise.
*/
public boolean hasDimensionalAnnotation() {
return getDimensionalAnnotation() != null;
}
/**
* @return the @Dimensional annotation if set, null otherwise.
*/
public Dimensional getDimensionalAnnotation() {
return AnnotationUtils.findAnnotation(method, Dimensional.class);
}
/**
* If the method has a @Query annotation.
*

View File

@@ -35,6 +35,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProper
import org.springframework.data.couchbase.repository.query.support.N1qlUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2013-2015 the original author or authors.
*
* 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.couchbase.repository.query;
import com.couchbase.client.java.view.SpatialViewQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.view.Dimensional;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.PartTree;
/**
* Execute a {@link Dimensional} repository query through the Spatial View mechanism.
*
* @author Simon Baslé
*/
public class SpatialViewBasedQuery implements RepositoryQuery {
private static final Logger LOG = LoggerFactory.getLogger(SpatialViewBasedQuery.class);
private final CouchbaseQueryMethod method;
private final CouchbaseOperations operations;
public SpatialViewBasedQuery(CouchbaseQueryMethod method, CouchbaseOperations operations) {
this.method = method;
this.operations = operations;
}
@Override
public Object execute(Object[] runtimeParams) {
String designDoc = method.getDimensionalAnnotation().designDocument();
String viewName = method.getDimensionalAnnotation().spatialViewName();
int dimensions = method.getDimensionalAnnotation().dimensions();
/*
here contrary to the classical view query we don't support not including an attribute of
the entity in the method name, those are mandatory and will result in a PropertyReferenceException
if not used...
*/
PartTree tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType());
//prepare a spatial view query to be used as a base for the query creator
SpatialViewQuery baseSpatialQuery = SpatialViewQuery.from(designDoc, viewName)
.stale(operations.getDefaultConsistency().viewConsistency());
//use the SpatialViewQueryCreator to complete it
SpatialViewQueryCreator creator = new SpatialViewQueryCreator(dimensions,
tree, new ParametersParameterAccessor(method.getParameters(), runtimeParams),
baseSpatialQuery, operations.getConverter());
SpatialViewQuery finalQuery = creator.createQuery();
//execute the spatial query
return execute(finalQuery);
}
protected Object execute(SpatialViewQuery query) {
if (LOG.isDebugEnabled()) {
LOG.debug("Executing spatial view query: " + query.toString());
}
return operations.findBySpatialView(query, method.getEntityInformation().getJavaType());
}
@Override
public CouchbaseQueryMethod getQueryMethod() {
return method;
}
}

View File

@@ -0,0 +1,240 @@
/*
* Copyright 2012-2015 the original author or authors
*
* 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.couchbase.repository.query;
import java.util.Iterator;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.view.SpatialViewQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.view.Dimensional;
import org.springframework.data.couchbase.repository.query.support.GeoUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.geo.Shape;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
/**
* A QueryCreator that will enrich a {@link SpatialViewQuery} using query derivation mechanisms
* and the parsed {@link PartTree}.
* <p>
* Support for query derivation keywords is limited, it is triggered by having a {@link Dimensional} annotation
* on the query method.
* <br/>
* Here are the {@link Part.Type} supported:
* <ul>
* <li><b>WITHIN:</b> finds elements contained in the provided {@link Shape}, {@link Point Point[] polygon},
* pair of {@link Point}s bounding box (lower left+upper right) or pair of raw {@link JsonArray} (discouraged as it
* leaks Couchbase specific class in your method signature, needs to be numerical data)</li>
* <li><b>NEAR:</b> finds elements near a provided {@link Point}, within the provided {@link Distance}</li>
* <li><b>GREATER_THAN, AFTER, GREATER_THAN_EQUALS</b>: adds a numerical element to the startRange and null to the endRange
* (useful for non geographic additional dimensions)</li>
* <li><b>LESS_THAN, BEFORE, LESS_THAN_EQUALS</b>: adds null to the startRange and a numerical element to the endRange
* (useful for non geographic additional dimensions)</li>
* <li><b>SIMPLE_PROPERTY (Is, Equals)</b>: adds a numerical element to both the startRange and the endRange
* (useful for non geographic additional dimensions)</li>
* <li><b>BETWEEN</b>: adds a numerical element to the startRange and a second numerical element to the endRange
* (useful for non geographic additional dimensions)</li>
* </ul>
* </p>
* Additionally, {@link PartTree#isLimiting()} will trigger usage of {@link SpatialViewQuery#limit(int) limit}.
*/
public class SpatialViewQueryCreator extends AbstractQueryCreator<SpatialViewQuery, SpatialViewQuery> {
private final SpatialViewQuery query;
private final PartTree tree;
private final CouchbaseConverter converter;
private final int dimensions;
private final JsonArray startRange;
private final JsonArray endRange;
public SpatialViewQueryCreator(int dimensions, PartTree tree, ParameterAccessor parameters, SpatialViewQuery query,
CouchbaseConverter converter) {
super(tree, parameters);
this.query = query;
this.tree = tree;
this.converter = converter;
this.dimensions = dimensions;
this.startRange = JsonArray.create();
this.endRange = JsonArray.create();
}
@Override
protected SpatialViewQuery create(Part part, Iterator<Object> objectIterator) {
ConvertingIterator iterator = new ConvertingIterator(objectIterator, converter);
switch (part.getType()) {
case WITHIN:
applyWithin(startRange, endRange, iterator);
break;
case NEAR:
applyNear(startRange, endRange, iterator);
break;
case GREATER_THAN:
case GREATER_THAN_EQUAL:
case AFTER:
startRange.add(checkedNext(iterator, Object.class, null));
endRange.addNull();
break;
case LESS_THAN:
case LESS_THAN_EQUAL:
case BEFORE:
startRange.addNull();
endRange.add(checkedNext(iterator, Object.class, null));
break;
case SIMPLE_PROPERTY:
Object equals = checkedNext(iterator, Object.class, null);
startRange.add(equals);
endRange.add(equals);
break;
case BETWEEN:
startRange.add(checkedNext(iterator, Object.class, null));
endRange.add(checkedNext(iterator, Object.class, null));
break;
default:
throw new IllegalArgumentException("Unsupported keyword in Spatial View query derivation: " + part.toString());
}
//will complete the ranges in complete step (if not enough elements for the ranges to match dimension count)
return query;
}
private static void completeRangeIfNeeded(JsonArray range, int dimensions) {
for (int i = range.size(); i < dimensions; i++) {
range.addNull();
}
}
private static void applyWithin(JsonArray startRange, JsonArray endRange, Iterator<Object> iterator) {
if (!iterator.hasNext()) {
throw new IllegalArgumentException("Not enough parameters for within");
}
Object next = iterator.next();
if (next instanceof Shape) {
GeoUtils.convertShapeTo2DRanges(startRange, endRange, (Shape) next);
} else if (next instanceof Point) {
//expect another point for the other corner of the bounding box
Point northwest = (Point) next;
Point southeast = checkedNext(iterator, Point.class, "Cannot compute a bounding box for within, 2 Point needed");
GeoUtils.convertPointsTo2DRanges(startRange, endRange, true, northwest, southeast);
} else if (next instanceof Point[]) {
GeoUtils.convertPointsTo2DRanges(startRange, endRange, false, (Point[]) next);
} else if (next instanceof JsonArray) { //discouraged, leaks Couchbase classes into signatures
JsonArray first = (JsonArray) next;
for (Object o : first) {
startRange.add(o);
}
JsonArray second = checkedNext(iterator, JsonArray.class, "2 JsonArray required for within: startRange and endRange");
for (Object o : second) {
endRange.add(o);
}
} else {
//TODO Collection<Point>: same as polygon
//TODO Collection<Double/Number>: one collection per range
//TODO Number[]: one array per range
throw new IllegalArgumentException("Unsupported parameter type for within: " + next.getClass());
}
}
private static void applyNear(JsonArray startRange, JsonArray endRange, Iterator<Object> iterator) {
if (!iterator.hasNext()) {
throw new IllegalArgumentException("Not enough parameters for near");
}
Point near = checkedNext(iterator, Point.class, "Near queries need a Point as first argument");
Distance distance = checkedNext(iterator, Distance.class, "Near queries need a maximum Distance as second argument");
double[] boundingBox = GeoUtils.getBoundingBoxForNear(near, distance);
startRange.add(boundingBox[0]).add(boundingBox[1]);
endRange.add(boundingBox[2]).add(boundingBox[3]);
}
/**
* Retrieve next value in the iterator assuming it is of the specified type (otherwise throw
* an {@link IllegalArgumentException}.
*
* @param iterator the iterator to peek into.
* @param clazz the expected type of the next value in the iterator.
* @param errorMsg the error message prefix to use in the exception (will append a short message
* describing if the iterator has no value or if the type found was different than expected).
* @param <T> the desired return type.
* @return the next value as a T.
* @throws IllegalArgumentException if there is no next value or it doesn't conform to the desired type.
*/
private static <T> T checkedNext(Iterator<?> iterator, Class<T> clazz, String errorMsg) {
if (errorMsg == null) {
errorMsg = "Expected an additional parameter of type " + clazz.getName();
}
if (!iterator.hasNext()) {
throw new IllegalArgumentException(errorMsg + ", missing parameter");
}
Object next = iterator.next();
if (clazz.isInstance(next)) {
return (T) next;
} else if (next == null) {
throw new IllegalArgumentException(errorMsg + ", got null");
} else {
throw new IllegalArgumentException(errorMsg + ", got a " + next.getClass().getName());
}
}
@Override
protected SpatialViewQuery and(Part part, SpatialViewQuery base, Iterator<Object> iterator) {
return create(part, iterator);//and not really supported, all query derivation mutate the original ViewQuery
}
@Override
protected SpatialViewQuery or(SpatialViewQuery base, SpatialViewQuery criteria) {
//this won't be called unless there's a Or keyword in the method
throw new UnsupportedOperationException("Or is not supported for View-based queries");
}
@Override
protected SpatialViewQuery complete(SpatialViewQuery criteria, Sort sort) {
if (sort != null) {
throw new IllegalArgumentException("Sort is not supported on Spatial View queries");
}
if (tree.isLimiting()) {
query.limit(tree.getMaxResults());
}
if (startRange.isEmpty() && endRange.isEmpty()) {
return query;
}
completeRangeIfNeeded(startRange, dimensions);
completeRangeIfNeeded(endRange, dimensions);
return query.range(startRange, endRange);
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.data.couchbase.repository.query;
import java.util.List;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.view.SpatialViewQuery;
import com.couchbase.client.java.view.Stale;
import com.couchbase.client.java.view.ViewQuery;
import com.couchbase.client.java.view.ViewResult;
@@ -28,6 +29,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Shape;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
@@ -81,23 +84,33 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
protected Object deriveAndExecute(Object[] runtimeParams) {
String designDoc = designDocName(method);
String viewName = method.getViewAnnotation().viewName();
//prepare a ViewQuery to be used as a base for the ViewQueryCreator
ViewQuery baseQuery = ViewQuery.from(designDoc, viewName)
.stale(operations.getDefaultConsistency().viewConsistency());
try {
PartTree tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType());
ViewQueryCreator creator = new ViewQueryCreator(tree,
new ParametersParameterAccessor(method.getParameters(), runtimeParams),
//use a ViewQueryCreator to complete the base query
ViewQueryCreator creator = new ViewQueryCreator(tree, new ParametersParameterAccessor(method.getParameters(), runtimeParams),
baseQuery, operations.getConverter());
ViewQuery query = creator.createQuery();
//detect count prefix in the method name and consider it triggers a reduce
if (tree.isCountProjection() == Boolean.TRUE) {
return executeReduce(query, designDoc, viewName);
} else {
//otherwise just execute the query
return execute(query);
}
} catch (PropertyReferenceException e) {
/*
For views, not including an attribute name in the method will result in returning
the whole set of results from the view.
This is detected by looking for PropertyReferenceExceptions that seem to complain
about a missing property that corresponds to the method name
*/
if (e.getPropertyName().equals(method.getName())) {
return execute(baseQuery);
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2012-2015 the original author or authors
*
* 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.couchbase.repository.query.support;
import com.couchbase.client.java.document.json.JsonArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.geo.Shape;
/**
* Utility class to deal with geo/dimensional indexed data and queries.
*
* @author Simon Baslé
*/
public class GeoUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(GeoUtils.class);
/**
* Computes the bounding box approximation for a "near" query (max distance from a point of origin).
*
* @param origin the point of origin, center for the query.
* @param distance the max distance to search within (negative distances will be multiplied by -1).
* @return the bounding box approximation for this query ([xMin, yMin, xMax, yMax]).
* @throws NullPointerException if any of the origin and distance are null
*/
public static double[] getBoundingBoxForNear(Point origin, Distance distance) {
if (origin == null || distance == null) throw new NullPointerException("Origin and distance required");
warnBoundingBoxApproximation("near a Point (within Distance)");
//since maxDistance COULD be negative, we have to make sure we have correct min/max
double maxDistance = Math.abs(distance.getNormalizedValue());
double xMin = origin.getX() - maxDistance;
double yMin = origin.getY() - maxDistance;
double xMax = origin.getX() + maxDistance;
double yMax = origin.getY() + maxDistance;
return new double[] { xMin, yMin, xMax, yMax };
}
/**
* Convert a sequence of {@link Point Points} describing a polygon to a pair of
* {@link JsonArray} ranges corresponding to that polygon's bounding box,
* and inject the coordinates into startRange and endRange.
* If it is already equivalent to a Box (upper-left Point + lower-right Point), set
* <i>isBoundingBox</i> to true.
* Otherwise, this method will attempt to find the bounding box by finding the lowest
* and highest X and Y coordinates.
*
* @param startRange the startRange to populate with this shape's data.
* @param endRange the endRange to populate with this shape's data.
* @param isBoundingBox true to skip finding min/max X and Y coordinates and use 2 Points as a {@link Box}.
* @param points the sequence of Points.
* @throws IllegalArgumentException if no points are provided, or in the case of isBoundingBox true
* if more or less than 2 points are provided or the 2 points are not ordered (a.x <= b.x && a.y <= b.y).
*/
public static void convertPointsTo2DRanges(JsonArray startRange, JsonArray endRange, boolean isBoundingBox, Point... points) {
if (points == null || points.length == 0) {
throw new IllegalArgumentException("Needs points to convert");
}
if (isBoundingBox) {
if (points.length != 2) {
throw new IllegalArgumentException("Bounding box must be made of 2 points");
}
if (points[0].getX() > points[1].getX() || points[0].getY() > points[1].getY()) {
throw new IllegalArgumentException("Bounding box must have point A on the lower left of point B");
}
startRange.add(points[0].getX()).add(points[0].getY());
endRange.add(points[1].getX()).add(points[1].getY());
} else {
//this is like a polygon, find the bounding box
warnBoundingBoxApproximation("within a sequence of Points (polygon)");
//find the lowest and highest X and Y to get the bounding box
double xMin = Double.POSITIVE_INFINITY;
double yMin = Double.POSITIVE_INFINITY;
double xMax = Double.NEGATIVE_INFINITY;
double yMax = Double.NEGATIVE_INFINITY;
for (Point point : points) {
xMin = point.getX() < xMin ? point.getX() : xMin;
xMax = point.getX() > xMax ? point.getX() : xMax;
yMin = point.getY() < yMin ? point.getY() : yMin;
yMax = point.getY() > yMax ? point.getY() : yMax;
}
//once we have the coordinates of lower left and upper right points, use them
startRange.add(xMin).add(yMin);
endRange.add(xMax).add(yMax);
}
}
/**
* Convert a {@link Shape} to a pair of {@link JsonArray} ranges, injected into startRange and endRange.
*
* @param startRange the startRange to populate with this shape's data.
* @param endRange the endRange to populate with this shape's data.
* @param shape the shape to extract ranges from.
* @throws IllegalArgumentException if the {@link Shape} is unsupported.
*/
public static void convertShapeTo2DRanges(JsonArray startRange, JsonArray endRange, Shape shape) {
if (shape instanceof Box) {
Box box = (Box) shape;
startRange //add minimum coordinates for x and y
.add(box.getFirst().getX())
.add(box.getFirst().getY());
endRange //add maximum coordinates for x and y
.add(box.getSecond().getX())
.add(box.getSecond().getY());
} else if (shape instanceof Polygon) {
warnBoundingBoxApproximation("within a Polygon");
//find the lowest and highest X and Y to get the bounding box
double xMin = Double.POSITIVE_INFINITY;
double yMin = Double.POSITIVE_INFINITY;
double xMax = Double.NEGATIVE_INFINITY;
double yMax = Double.NEGATIVE_INFINITY;
for (Point point : (Polygon) shape) {
xMin = point.getX() < xMin ? point.getX() : xMin;
xMax = point.getX() > xMax ? point.getX() : xMax;
yMin = point.getY() < yMin ? point.getY() : yMin;
yMax = point.getY() > yMax ? point.getY() : yMax;
}
//once we have the coordinates of lower left and upper right points, use them
startRange.add(xMin).add(yMin);
endRange.add(xMax).add(yMax);
} else if (shape instanceof Circle) {
warnBoundingBoxApproximation("within a Circle");
//here the bounding box is the box that contains the circle
Circle circle = (Circle) shape;
Point center = circle.getCenter();
double radius = circle.getRadius().getNormalizedValue();
double xMin = center.getX() - radius;
double xMax = center.getX() + radius;
double yMin = center.getY() - radius;
double yMax = center.getY() + radius;
startRange.add(xMin).add(yMin);
endRange.add(xMax).add(yMax);
} else {
throw new IllegalArgumentException("Unsupported shape " + shape.getClass().getName());
}
}
private static void warnBoundingBoxApproximation(String kind) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Spatial View queries " + kind + " are made using a bounding box approximation");
}
}
}

View File

@@ -32,6 +32,7 @@ import org.springframework.data.couchbase.repository.config.RepositoryOperations
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod;
import org.springframework.data.couchbase.repository.query.PartTreeN1qlBasedQuery;
import org.springframework.data.couchbase.repository.query.SpatialViewBasedQuery;
import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
import org.springframework.data.couchbase.repository.query.ViewBasedCouchbaseQuery;
import org.springframework.data.mapping.context.MappingContext;
@@ -184,7 +185,9 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, mappingContext);
String namedQueryName = queryMethod.getNamedQueryName();
if (queryMethod.hasViewAnnotation()) {
if (queryMethod.hasDimensionalAnnotation()) {
return new SpatialViewBasedQuery(queryMethod, couchbaseOperations);
} else if (queryMethod.hasViewAnnotation()) {
return new ViewBasedCouchbaseQuery(queryMethod, couchbaseOperations);
} else if (queryMethod.hasN1qlAnnotation()) {
if (queryMethod.hasInlineN1qlQuery()) {