DATACOUCH-165 - Add utilities to remove geo false positives

PointInShapeEvaluator can be used to check answers and remove false positives from answers that have been approximated (circle and polygon).

One implementations are provided, based on awt.

Hinted at the existence of the PointInShapeEvaluator in the message that warns about bounding box approximations.

Added mention of the AwtPointInShapeEvaluator in the asciidoc with code sample.
This commit is contained in:
Simon Baslé
2015-10-09 18:42:51 +02:00
parent a2f26f4310
commit c6aa84ced4
6 changed files with 592 additions and 2 deletions

View File

@@ -0,0 +1,89 @@
/*
* 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 java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.List;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
/**
* A default {@link PointInShapeEvaluator} implementation based on the JDK's java.awt.geom classes.
*
* @author Simon Baslé
*/
public class AwtPointInShapeEvaluator extends PointInShapeEvaluator {
@Override
public boolean pointInPolygon(Point p, Polygon polygon) {
final List<Point> points = polygon.getPoints();
Path2D awtPolygon = new Path2D.Double(Path2D.WIND_EVEN_ODD, points.size());
boolean first = true;
for (Point point : points) {
if (first) {
first = false;
awtPolygon.moveTo(point.getX(), point.getY());
} else {
awtPolygon.lineTo(point.getX(), point.getY());
}
}
awtPolygon.closePath();
return pointInPolygon(p, awtPolygon);
}
@Override
public boolean pointInPolygon(Point p, Point... points) {
if (points == null) throw new NullPointerException("Polygon must at least contain 3 points");
if (points.length < 3) throw new IllegalArgumentException("Polygon must at least contain 3 points");
Path2D awtPolygon = new Path2D.Double(Path2D.WIND_EVEN_ODD, points.length);
boolean first = true;
for (Point point : points) {
if (first) {
first = false;
awtPolygon.moveTo(point.getX(), point.getY());
} else {
awtPolygon.lineTo(point.getX(), point.getY());
}
}
awtPolygon.closePath();
return pointInPolygon(p, awtPolygon);
}
@Override
public boolean pointInCircle(Point p, Circle c) {
Point2D center = new Point2D.Double(c.getCenter().getX(), c.getCenter().getY());
return pointNear(p, center, c.getRadius().getNormalizedValue());
}
@Override
public boolean pointInCircle(Point p, Point center, Distance radiusDistance) {
double radius = radiusDistance.getNormalizedValue();
return pointNear(p, new Point2D.Double(center.getX(), center.getY()), radius);
}
private boolean pointInPolygon(Point p, Path2D awtPolygon) {
return awtPolygon.contains(p.getX(), p.getY());
}
private boolean pointNear(Point p, Point2D center, double distance) {
return center.distance(p.getX(), p.getY()) <= distance;
}
}

View File

@@ -166,7 +166,8 @@ public class GeoUtils {
private static void warnBoundingBoxApproximation(String kind) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Spatial View queries " + kind + " are made using a bounding box approximation");
LOGGER.debug("Spatial View queries " + kind + " are made using a bounding box approximation," +
"you probably want to remove false positives by applying a PointInShapeEvaluator");
}
}
}

View File

@@ -0,0 +1,194 @@
/*
* 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 java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.core.convert.converter.Converter;
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;
/**
* PointInShapeEvaluator can be used to tell if a particular {@link Point} is contained by a
* {@link Polygon} or {@link Circle}. It is most useful to eliminate false positive when a
* geo query has been made using a {@link Box bounding box} approximation.
*
* For the purpose of this class, a point on the edge of a polygon isn't considered within it.
* On the contrary a point on the edge of a circle is considered in it (distance from center <= radius).
*
* To that end, {@link #removeFalsePositives(Collection, Converter, Circle) additional methods}
* that return a {@link List} of objects with false positives removed are also provided. However,
* these need a {@link Converter Converter&lt;T, Point&gt;} to extract the location attribute that
* should be tested against the polygon/circle.
*
* @author Simon Baslé
* @see AwtPointInShapeEvaluator for a simple implementation based on AWT standard library.
*/
public abstract class PointInShapeEvaluator {
/**
* Determine if a {@link Point} is contained by a {@link Polygon}.
*
* @param p the point to test.
* @param polygon the polygon we want the point to be in.
* @return true if the polygon contains the point, false otherwise.
*/
public abstract boolean pointInPolygon(Point p, Polygon polygon);
/**
* Determine if a {@link Point} is contained by a polygon represented
* as an array of {@link Point points}.
*
* The points are not required to form a closed shape, but can (by having the
* first and last points be the same).
*
* @param p the point to test.
* @param points the Point[] representation of the polygon we want the point to be in.
* @return true if the polygon contains the point, false otherwise.
*/
public abstract boolean pointInPolygon(Point p, Point... points);
/**
* Determine if a {@link Point} is contained by a {@link Circle}.
*
* @param p the point to test.
* @param c the Circle we want the point to be in.
* @return true if the circle contains the point, false otherwise.
*/
public abstract boolean pointInCircle(Point p, Circle c);
/**
* Determine if a {@link Point} is contained by a {@link Circle} represented
* by its {@link Point center Point} and {@link Distance Distance radius}.
*
* @param p the point to test.
* @param center the center Point of the Circle we want the point to be in.
* @param radius the Distance radius of the Circle we want the point to be in.
* @return true if the circle contains the point, false otherwise.
*/
public abstract boolean pointInCircle(Point p, Point center, Distance radius);
/**
* Utility method to remove false positives from a collection of objects that have a
* notion of location, where we want to only include values that are located within a polygon.
*
* The collection should usually be already contained in the bounding box approximation of
* the polygon for maximum efficiency.
*
* @param boundingBoxResults the collections of located objects approximately inside the target polygon.
* @param locationExtractor a {@link Converter} to extract the location of the value objects.
* @param polygon the target polygon.
* @param <T> the type of located value objects in the collection.
* @return a {@link List} of the value objects which location has been verified to actually be contained within the polygon.
*/
public <T> List<T> removeFalsePositives(Collection<? extends T> boundingBoxResults,
Converter<T, Point> locationExtractor, Polygon polygon) {
ArrayList<T> result = new ArrayList<T>(boundingBoxResults.size());
for (T boxResult : boundingBoxResults) {
Point p = locationExtractor.convert(boxResult);
if (pointInPolygon(p, polygon)) {
result.add(boxResult);
}
}
result.trimToSize();
return result;
}
/**
* Utility method to remove false positives from a collection of objects that have a
* notion of location, where we want to only include values that are located within a circle.
*
* The collection should usually be already contained in the bounding box approximation of
* the circle for maximum efficiency.
*
* @param boundingBoxResults the collections of located objects approximately inside the target circle.
* @param locationExtractor a {@link Converter} to extract the location of the value objects.
* @param circle the target circle.
* @param <T> the type of located value objects in the collection.
* @return a {@link List} of the value objects which location has been verified to actually be contained within the circle.
*/
public <T> List<T> removeFalsePositives(Collection<? extends T> boundingBoxResults,
Converter<T, Point> locationExtractor, Circle circle) {
ArrayList<T> result = new ArrayList<T>(boundingBoxResults.size());
for (T boxResult : boundingBoxResults) {
Point p = locationExtractor.convert(boxResult);
if (pointInCircle(p, circle)) {
result.add(boxResult);
}
}
result.trimToSize();
return result;
}
/**
* Utility method to remove false positives from a collection of objects that have a
* notion of location, where we want to only include values that are located within a polygon.
*
* The collection should usually be already contained in the bounding box approximation of
* the polygon for maximum efficiency.
*
* @param boundingBoxResults the collections of located objects approximately inside the target polygon.
* @param locationExtractor a {@link Converter} to extract the location of the value objects.
* @param polygon the target polygon, as an array of {@link Point} (not necessarily closed).
* @param <T> the type of located value objects in the collection.
* @return a {@link List} of the value objects which location has been verified to actually be contained within the polygon.
*/
public <T> List<T> removeFalsePositives(Collection<? extends T> boundingBoxResults,
Converter<T, Point> locationExtractor, Point... polygon) {
ArrayList<T> result = new ArrayList<T>(boundingBoxResults.size());
for (T boxResult : boundingBoxResults) {
Point p = locationExtractor.convert(boxResult);
if (pointInPolygon(p, polygon)) {
result.add(boxResult);
}
}
result.trimToSize();
return result;
}
/**
* Utility method to remove false positives from a collection of objects that have a
* notion of location, where we want to only include values that are located within a circle.
*
* The collection should usually be already contained in the bounding box approximation of
* the circle for maximum efficiency.
*
* @param boundingBoxResults the collections of located objects approximately inside the target circle.
* @param locationExtractor a {@link Converter} to extract the location of the value objects.
* @param center the center of the target circle.
* @param radius the radius of the target circle.
* @param <T> the type of located value objects in the collection.
* @return a {@link List} of the value objects which location has been verified to actually be contained within the circle.
*/
public <T> List<T> removeFalsePositives(Collection<? extends T> boundingBoxResults,
Converter<T, Point> locationExtractor, Point center, Distance radius) {
ArrayList<T> result = new ArrayList<T>(boundingBoxResults.size());
for (T boxResult : boundingBoxResults) {
Point p = locationExtractor.convert(boxResult);
if (pointInCircle(p, center, radius)) {
result.add(boxResult);
}
}
result.trimToSize();
return result;
}
}