LUCENE-8632: New XYShape Field and Queries for indexing and searching general cartesian geometries

The LatLonShape field and LatLonShape query classes added the ability to index and search geospatial
geometries in the WGS-84 latitude, longitude coordinate reference system. The foundation for this
capability is provided by the Tessellator that converts an array of vertices describing a Point Line
or Polygon into a stream of 3 vertex triangles that are encoded as a seven dimension point and
indexed using the BKD POINT structure. A nice property of the Tessellator is that lat, lon
restrictions are artificial and really only bound by the API.

This commit builds on top of / abstracts the Tessellator LatLonShape and LatLonShapeQuery classes to
provide the ability to index & search general cartesian (non WGS84 lat,lon restricted) geometry.
It does so by introducing two new base classes: ShapeField and ShapeQuery that provide the indexing
and search foundation for LatLonShape and the LatLonShape derived query classes
(LatLonShapeBoundingBoxQuery, LatLonShapeLineQuery, LatLonShapePolygonQuery) and introducing a new
XYShape factory class along with XYShape derived query classes (XYShapeBoundingBoxQuery,
XYShapeLineQuery, XYShapePolygonQuery). The heart of the cartesian indexing is achieved through
XYShapeEncodingUtils that converts the double precision vertices into an integer encoded seven
dimension point (similar to LatLonShape).

The test framework is also further abstracted and extended to provide a full test suite for the
new XYShape capability that works the same way as the LatLonShape test suite (but applied to non
GIS geometries).
This commit is contained in:
Nicholas Knize 2019-07-08 14:59:37 -05:00
parent ac209b637d
commit 0c09481374
45 changed files with 4440 additions and 1529 deletions

View File

@ -65,6 +65,9 @@ API Changes
New Features New Features
* LUCENE-8632: New XYShape Field and Queries for indexing and searching general cartesian
geometries. (Nick Knize)
* LUCENE-8891: Snowball stemmer/analyzer for the Estonian language. * LUCENE-8891: Snowball stemmer/analyzer for the Estonian language.
(Gert Morten Paimla via Tomoko Uchida) (Gert Morten Paimla via Tomoko Uchida)

View File

@ -29,18 +29,22 @@ import org.apache.lucene.index.PointValues.Relation;
*/ */
// Both Polygon.contains() and Polygon.crossesSlowly() loop all edges, and first check that the edge is within a range. // Both Polygon.contains() and Polygon.crossesSlowly() loop all edges, and first check that the edge is within a range.
// we just organize the edges to do the same computations on the same subset of edges more efficiently. // we just organize the edges to do the same computations on the same subset of edges more efficiently.
public final class Polygon2D extends EdgeTree { public class Polygon2D extends EdgeTree {
// each component/hole is a node in an augmented 2d kd-tree: we alternate splitting between latitude/longitude, // each component/hole is a node in an augmented 2d kd-tree: we alternate splitting between latitude/longitude,
// and pull up max values for both dimensions to each parent node (regardless of split). // and pull up max values for both dimensions to each parent node (regardless of split).
/** tree of holes, or null */ /** tree of holes, or null */
private final Polygon2D holes; protected final Polygon2D holes;
private final AtomicBoolean containsBoundary = new AtomicBoolean(false); private final AtomicBoolean containsBoundary = new AtomicBoolean(false);
private Polygon2D(Polygon polygon, Polygon2D holes) { protected Polygon2D(final double minLat, final double maxLat, final double minLon, final double maxLon, double[] lats, double[] lons, Polygon2D holes) {
super(polygon.minLat, polygon.maxLat, polygon.minLon, polygon.maxLon, polygon.getPolyLats(), polygon.getPolyLons()); super(minLat, maxLat, minLon, maxLon, lats, lons);
this.holes = holes; this.holes = holes;
} }
protected Polygon2D(Polygon polygon, Polygon2D holes) {
this(polygon.minLat, polygon.maxLat, polygon.minLon, polygon.maxLon, polygon.getPolyLats(), polygon.getPolyLons(), holes);
}
/** /**
* Returns true if the point is contained within this polygon. * Returns true if the point is contained within this polygon.
* <p> * <p>

View File

@ -19,31 +19,29 @@ package org.apache.lucene.document;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.lucene.geo.GeoUtils; import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
import org.apache.lucene.document.ShapeField.Triangle;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Tessellator; import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.geo.Tessellator.Triangle; import org.apache.lucene.index.PointValues; // javadoc
import org.apache.lucene.index.PointValues;
import org.apache.lucene.search.Query; import org.apache.lucene.search.Query;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
/** /**
* An indexed shape utility class. * An geo shape utility class for indexing and searching gis geometries
* whose vertices are latitude, longitude values (in decimal degrees).
* <p> * <p>
* {@link Polygon}'s are decomposed into a triangular mesh using the {@link Tessellator} utility class * This class defines six static factory methods for common indexing and search operations:
* Each {@link Triangle} is encoded and indexed as a multi-value field.
* <p>
* Finding all shapes that intersect a range (e.g., bounding box) at search time is efficient.
* <p>
* This class defines static factory methods for common operations:
* <ul> * <ul>
* <li>{@link #createIndexableFields(String, Polygon)} for matching polygons that intersect a bounding box. * <li>{@link #createIndexableFields(String, Polygon)} for indexing a geo polygon.
* <li>{@link #newBoxQuery newBoxQuery()} for matching polygons that intersect a bounding box. * <li>{@link #createIndexableFields(String, Line)} for indexing a geo linestring.
* <li>{@link #createIndexableFields(String, double, double)} for indexing a lat, lon geo point.
* <li>{@link #newBoxQuery newBoxQuery()} for matching geo shapes that have some {@link QueryRelation} with a bounding box.
* <li>{@link #newLineQuery newLineQuery()} for matching geo shapes that have some {@link QueryRelation} with a linestring.
* <li>{@link #newPolygonQuery newPolygonQuery()} for matching geo shapes that have some {@link QueryRelation} with a polygon.
* </ul> * </ul>
* <b>WARNING</b>: Like {@link LatLonPoint}, vertex values are indexed with some loss of precision from the * <b>WARNING</b>: Like {@link LatLonPoint}, vertex values are indexed with some loss of precision from the
@ -55,13 +53,6 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
* @lucene.experimental * @lucene.experimental
*/ */
public class LatLonShape { public class LatLonShape {
static final int BYTES = Integer.BYTES;
protected static final FieldType TYPE = new FieldType();
static {
TYPE.setDimensions(7, 4, BYTES);
TYPE.freeze();
}
// no instance: // no instance:
private LatLonShape() { private LatLonShape() {
@ -70,10 +61,10 @@ public class LatLonShape {
/** create indexable fields for polygon geometry */ /** create indexable fields for polygon geometry */
public static Field[] createIndexableFields(String fieldName, Polygon polygon) { public static Field[] createIndexableFields(String fieldName, Polygon polygon) {
// the lionshare of the indexing is done by the tessellator // the lionshare of the indexing is done by the tessellator
List<Triangle> tessellation = Tessellator.tessellate(polygon); List<Tessellator.Triangle> tessellation = Tessellator.tessellate(polygon);
List<LatLonTriangle> fields = new ArrayList<>(); List<Triangle> fields = new ArrayList<>();
for (Triangle t : tessellation) { for (Tessellator.Triangle t : tessellation) {
fields.add(new LatLonTriangle(fieldName, t)); fields.add(new Triangle(fieldName, t));
} }
return fields.toArray(new Field[fields.size()]); return fields.toArray(new Field[fields.size()]);
} }
@ -84,286 +75,38 @@ public class LatLonShape {
Field[] fields = new Field[numPoints - 1]; Field[] fields = new Field[numPoints - 1];
// create "flat" triangles // create "flat" triangles
for (int i = 0, j = 1; j < numPoints; ++i, ++j) { for (int i = 0, j = 1; j < numPoints; ++i, ++j) {
fields[i] = new LatLonTriangle(fieldName, line.getLat(i), line.getLon(i), line.getLat(j), line.getLon(j), line.getLat(i), line.getLon(i)); fields[i] = new Triangle(fieldName,
encodeLongitude(line.getLon(i)), encodeLatitude(line.getLat(i)),
encodeLongitude(line.getLon(j)), encodeLatitude(line.getLat(j)),
encodeLongitude(line.getLon(i)), encodeLatitude(line.getLat(i)));
} }
return fields; return fields;
} }
/** create indexable fields for point geometry */ /** create indexable fields for point geometry */
public static Field[] createIndexableFields(String fieldName, double lat, double lon) { public static Field[] createIndexableFields(String fieldName, double lat, double lon) {
return new Field[] {new LatLonTriangle(fieldName, lat, lon, lat, lon, lat, lon)}; return new Field[] {new Triangle(fieldName,
encodeLongitude(lon), encodeLatitude(lat),
encodeLongitude(lon), encodeLatitude(lat),
encodeLongitude(lon), encodeLatitude(lat))};
} }
/** create a query to find all polygons that intersect a defined bounding box /** create a query to find all indexed geo shapes that intersect a defined bounding box **/
**/
public static Query newBoxQuery(String field, QueryRelation queryRelation, double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) { public static Query newBoxQuery(String field, QueryRelation queryRelation, double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) {
return new LatLonShapeBoundingBoxQuery(field, queryRelation, minLatitude, maxLatitude, minLongitude, maxLongitude); return new LatLonShapeBoundingBoxQuery(field, queryRelation, minLatitude, maxLatitude, minLongitude, maxLongitude);
} }
/** create a query to find all polygons that intersect a provided linestring (or array of linestrings) /** create a query to find all indexed geo shapes that intersect a provided linestring (or array of linestrings)
* note: does not support dateline crossing * note: does not support dateline crossing
**/ **/
public static Query newLineQuery(String field, QueryRelation queryRelation, Line... lines) { public static Query newLineQuery(String field, QueryRelation queryRelation, Line... lines) {
return new LatLonShapeLineQuery(field, queryRelation, lines); return new LatLonShapeLineQuery(field, queryRelation, lines);
} }
/** create a query to find all polygons that intersect a provided polygon (or array of polygons) /** create a query to find all indexed geo shapes that intersect a provided polygon (or array of polygons)
* note: does not support dateline crossing * note: does not support dateline crossing
**/ **/
public static Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) { public static Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) {
return new LatLonShapePolygonQuery(field, queryRelation, polygons); return new LatLonShapePolygonQuery(field, queryRelation, polygons);
} }
/** polygons are decomposed into tessellated triangles using {@link org.apache.lucene.geo.Tessellator}
* these triangles are encoded and inserted as separate indexed POINT fields
*/
private static class LatLonTriangle extends Field {
LatLonTriangle(String name, double aLat, double aLon, double bLat, double bLon, double cLat, double cLon) {
super(name, TYPE);
setTriangleValue(encodeLongitude(aLon), encodeLatitude(aLat), encodeLongitude(bLon), encodeLatitude(bLat), encodeLongitude(cLon), encodeLatitude(cLat));
}
LatLonTriangle(String name, Triangle t) {
super(name, TYPE);
setTriangleValue(t.getEncodedX(0), t.getEncodedY(0), t.getEncodedX(1), t.getEncodedY(1), t.getEncodedX(2), t.getEncodedY(2));
}
public void setTriangleValue(int aX, int aY, int bX, int bY, int cX, int cY) {
final byte[] bytes;
if (fieldsData == null) {
bytes = new byte[7 * BYTES];
fieldsData = new BytesRef(bytes);
} else {
bytes = ((BytesRef) fieldsData).bytes;
}
encodeTriangle(bytes, aY, aX, bY, bX, cY, cX);
}
}
/** Query Relation Types **/
public enum QueryRelation {
INTERSECTS, WITHIN, DISJOINT
}
private static final int MINY_MINX_MAXY_MAXX_Y_X = 0;
private static final int MINY_MINX_Y_X_MAXY_MAXX = 1;
private static final int MAXY_MINX_Y_X_MINY_MAXX = 2;
private static final int MAXY_MINX_MINY_MAXX_Y_X = 3;
private static final int Y_MINX_MINY_X_MAXY_MAXX = 4;
private static final int Y_MINX_MINY_MAXX_MAXY_X = 5;
private static final int MAXY_MINX_MINY_X_Y_MAXX = 6;
private static final int MINY_MINX_Y_MAXX_MAXY_X = 7;
/**
* A triangle is encoded using 6 points and an extra point with encoded information in three bits of how to reconstruct it.
* Triangles are encoded with CCW orientation and might be rotated to limit the number of possible reconstructions to 2^3.
* Reconstruction always happens from west to east.
*/
public static void encodeTriangle(byte[] bytes, int aLat, int aLon, int bLat, int bLon, int cLat, int cLon) {
assert bytes.length == 7 * BYTES;
int aX;
int bX;
int cX;
int aY;
int bY;
int cY;
//change orientation if CW
if (GeoUtils.orient(aLon, aLat, bLon, bLat, cLon, cLat) == -1) {
aX = cLon;
bX = bLon;
cX = aLon;
aY = cLat;
bY = bLat;
cY = aLat;
} else {
aX = aLon;
bX = bLon;
cX = cLon;
aY = aLat;
bY = bLat;
cY = cLat;
}
//rotate edges and place minX at the beginning
if (bX < aX || cX < aX) {
if (bX < cX) {
int tempX = aX;
int tempY = aY;
aX = bX;
aY = bY;
bX = cX;
bY = cY;
cX = tempX;
cY = tempY;
} else if (cX < aX) {
int tempX = aX;
int tempY = aY;
aX = cX;
aY = cY;
cX = bX;
cY = bY;
bX = tempX;
bY = tempY;
}
} else if (aX == bX && aX == cX) {
//degenerated case, all points with same longitude
//we need to prevent that aX is in the middle (not part of the MBS)
if (bY < aY || cY < aY) {
if (bY < cY) {
int tempX = aX;
int tempY = aY;
aX = bX;
aY = bY;
bX = cX;
bY = cY;
cX = tempX;
cY = tempY;
} else if (cY < aY) {
int tempX = aX;
int tempY = aY;
aX = cX;
aY = cY;
cX = bX;
cY = bY;
bX = tempX;
bY = tempY;
}
}
}
int minX = aX;
int minY = StrictMath.min(aY, StrictMath.min(bY, cY));
int maxX = StrictMath.max(aX, StrictMath.max(bX, cX));
int maxY = StrictMath.max(aY, StrictMath.max(bY, cY));
int bits, x, y;
if (minY == aY) {
if (maxY == bY && maxX == bX) {
y = cY;
x = cX;
bits = MINY_MINX_MAXY_MAXX_Y_X;
} else if (maxY == cY && maxX == cX) {
y = bY;
x = bX;
bits = MINY_MINX_Y_X_MAXY_MAXX;
} else {
y = bY;
x = cX;
bits = MINY_MINX_Y_MAXX_MAXY_X;
}
} else if (maxY == aY) {
if (minY == bY && maxX == bX) {
y = cY;
x = cX;
bits = MAXY_MINX_MINY_MAXX_Y_X;
} else if (minY == cY && maxX == cX) {
y = bY;
x = bX;
bits = MAXY_MINX_Y_X_MINY_MAXX;
} else {
y = cY;
x = bX;
bits = MAXY_MINX_MINY_X_Y_MAXX;
}
} else if (maxX == bX && minY == bY) {
y = aY;
x = cX;
bits = Y_MINX_MINY_MAXX_MAXY_X;
} else if (maxX == cX && maxY == cY) {
y = aY;
x = bX;
bits = Y_MINX_MINY_X_MAXY_MAXX;
} else {
throw new IllegalArgumentException("Could not encode the provided triangle");
}
NumericUtils.intToSortableBytes(minY, bytes, 0);
NumericUtils.intToSortableBytes(minX, bytes, BYTES);
NumericUtils.intToSortableBytes(maxY, bytes, 2 * BYTES);
NumericUtils.intToSortableBytes(maxX, bytes, 3 * BYTES);
NumericUtils.intToSortableBytes(y, bytes, 4 * BYTES);
NumericUtils.intToSortableBytes(x, bytes, 5 * BYTES);
NumericUtils.intToSortableBytes(bits, bytes, 6 * BYTES);
}
/**
* Decode a triangle encoded by {@link LatLonShape#encodeTriangle(byte[], int, int, int, int, int, int)}.
*/
public static void decodeTriangle(byte[] t, int[] triangle) {
assert triangle.length == 6;
int bits = NumericUtils.sortableBytesToInt(t, 6 * LatLonShape.BYTES);
//extract the first three bits
int tCode = (((1 << 3) - 1) & (bits >> 0));
switch (tCode) {
case MINY_MINX_MAXY_MAXX_Y_X:
triangle[0] = NumericUtils.sortableBytesToInt(t, 0 * LatLonShape.BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * LatLonShape.BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 2 * LatLonShape.BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * LatLonShape.BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 4 * LatLonShape.BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * LatLonShape.BYTES);
break;
case MINY_MINX_Y_X_MAXY_MAXX:
triangle[0] = NumericUtils.sortableBytesToInt(t, 0 * LatLonShape.BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * LatLonShape.BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 4 * LatLonShape.BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * LatLonShape.BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * LatLonShape.BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * LatLonShape.BYTES);
break;
case MAXY_MINX_Y_X_MINY_MAXX:
triangle[0] = NumericUtils.sortableBytesToInt(t, 2 * LatLonShape.BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * LatLonShape.BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 4 * LatLonShape.BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * LatLonShape.BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 0 * LatLonShape.BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * LatLonShape.BYTES);
break;
case MAXY_MINX_MINY_MAXX_Y_X:
triangle[0] = NumericUtils.sortableBytesToInt(t, 2 * LatLonShape.BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * LatLonShape.BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * LatLonShape.BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * LatLonShape.BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 4 * LatLonShape.BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * LatLonShape.BYTES);
break;
case Y_MINX_MINY_X_MAXY_MAXX:
triangle[0] = NumericUtils.sortableBytesToInt(t, 4 * LatLonShape.BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * LatLonShape.BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * LatLonShape.BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * LatLonShape.BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * LatLonShape.BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * LatLonShape.BYTES);
break;
case Y_MINX_MINY_MAXX_MAXY_X:
triangle[0] = NumericUtils.sortableBytesToInt(t, 4 * LatLonShape.BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * LatLonShape.BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * LatLonShape.BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * LatLonShape.BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * LatLonShape.BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * LatLonShape.BYTES);
break;
case MAXY_MINX_MINY_X_Y_MAXX:
triangle[0] = NumericUtils.sortableBytesToInt(t, 2 * LatLonShape.BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * LatLonShape.BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * LatLonShape.BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * LatLonShape.BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 4 * LatLonShape.BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * LatLonShape.BYTES);
break;
case MINY_MINX_Y_MAXX_MAXY_X:
triangle[0] = NumericUtils.sortableBytesToInt(t, 0 * LatLonShape.BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * LatLonShape.BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 4 * LatLonShape.BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * LatLonShape.BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * LatLonShape.BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * LatLonShape.BYTES);
break;
default:
throw new IllegalArgumentException("Could not decode the provided triangle");
}
//Points of the decoded triangle must be co-planar or CCW oriented
assert GeoUtils.orient(triangle[1], triangle[0], triangle[3], triangle[2], triangle[5], triangle[4]) >= 0;
}
} }

View File

@ -16,23 +16,24 @@
*/ */
package org.apache.lucene.document; package org.apache.lucene.document;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Rectangle; import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.geo.Rectangle2D; import org.apache.lucene.geo.Rectangle2D;
import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.index.PointValues.Relation;
/** /**
* Finds all previously indexed shapes that intersect the specified bounding box. * Finds all previously indexed geo shapes that intersect the specified bounding box.
* *
* <p>The field must be indexed using * <p>The field must be indexed using
* {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document. * {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document.
* *
* @lucene.experimental * @lucene.experimental
**/ **/
final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery { final class LatLonShapeBoundingBoxQuery extends ShapeQuery {
final Rectangle rectangle; final Rectangle rectangle;
final Rectangle2D rectangle2D; final Rectangle2D rectangle2D;
public LatLonShapeBoundingBoxQuery(String field, LatLonShape.QueryRelation queryRelation, double minLat, double maxLat, double minLon, double maxLon) { public LatLonShapeBoundingBoxQuery(String field, QueryRelation queryRelation, double minLat, double maxLat, double minLon, double maxLon) {
super(field, queryRelation); super(field, queryRelation);
this.rectangle = new Rectangle(minLat, maxLat, minLon, maxLon); this.rectangle = new Rectangle(minLat, maxLat, minLon, maxLon);
this.rectangle2D = Rectangle2D.create(this.rectangle); this.rectangle2D = Rectangle2D.create(this.rectangle);
@ -46,9 +47,9 @@ final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery {
/** returns true if the query matches the encoded triangle */ /** returns true if the query matches the encoded triangle */
@Override @Override
protected boolean queryMatches(byte[] t, int[] scratchTriangle, LatLonShape.QueryRelation queryRelation) { protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) {
// decode indexed triangle // decode indexed triangle
LatLonShape.decodeTriangle(t, scratchTriangle); ShapeField.decodeTriangle(t, scratchTriangle);
int aY = scratchTriangle[0]; int aY = scratchTriangle[0];
int aX = scratchTriangle[1]; int aX = scratchTriangle[1];
@ -57,7 +58,7 @@ final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery {
int cY = scratchTriangle[4]; int cY = scratchTriangle[4];
int cX = scratchTriangle[5]; int cX = scratchTriangle[5];
if (queryRelation == LatLonShape.QueryRelation.WITHIN) { if (queryRelation == QueryRelation.WITHIN) {
return rectangle2D.containsTriangle(aX, aY, bX, bY, cX, cY); return rectangle2D.containsTriangle(aX, aY, bX, bY, cX, cY);
} }
return rectangle2D.intersectsTriangle(aX, aY, bX, bY, cX, cY); return rectangle2D.intersectsTriangle(aX, aY, bX, bY, cX, cY);

View File

@ -18,7 +18,7 @@ package org.apache.lucene.document;
import java.util.Arrays; import java.util.Arrays;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Line2D;
@ -26,7 +26,7 @@ import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.util.NumericUtils; import org.apache.lucene.util.NumericUtils;
/** /**
* Finds all previously indexed shapes that intersect the specified arbitrary {@code Line}. * Finds all previously indexed geo shapes that intersect the specified arbitrary {@code Line}.
* <p> * <p>
* Note: * Note:
* <ul> * <ul>
@ -43,7 +43,7 @@ import org.apache.lucene.util.NumericUtils;
* *
* @lucene.experimental * @lucene.experimental
**/ **/
final class LatLonShapeLineQuery extends LatLonShapeQuery { final class LatLonShapeLineQuery extends ShapeQuery {
final Line[] lines; final Line[] lines;
final private Line2D line2D; final private Line2D line2D;
@ -85,7 +85,7 @@ final class LatLonShapeLineQuery extends LatLonShapeQuery {
@Override @Override
protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) { protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) {
LatLonShape.decodeTriangle(t, scratchTriangle); ShapeField.decodeTriangle(t, scratchTriangle);
double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle[0]); double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle[0]);
double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle[1]); double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle[1]);
@ -94,7 +94,7 @@ final class LatLonShapeLineQuery extends LatLonShapeQuery {
double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle[4]); double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle[4]);
double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle[5]); double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle[5]);
if (queryRelation == LatLonShape.QueryRelation.WITHIN) { if (queryRelation == QueryRelation.WITHIN) {
return line2D.relateTriangle(alon, alat, blon, blat, clon, clat) == Relation.CELL_INSIDE_QUERY; return line2D.relateTriangle(alon, alat, blon, blat, clon, clat) == Relation.CELL_INSIDE_QUERY;
} }
// INTERSECTS // INTERSECTS

View File

@ -18,7 +18,7 @@ package org.apache.lucene.document;
import java.util.Arrays; import java.util.Arrays;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Polygon2D;
@ -26,14 +26,19 @@ import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.util.NumericUtils; import org.apache.lucene.util.NumericUtils;
/** /**
* Finds all previously indexed shapes that intersect the specified arbitrary. * Finds all previously indexed geo shapes that intersect the specified arbitrary.
* <p>
* Note:
* <ul>
* <li>Dateline crossing is not yet supported. Polygons should be cut at the dateline and provided as a multipolygon query</li>
* </ul>
* *
* <p>The field must be indexed using * <p>The field must be indexed using
* {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document. * {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document.
* *
* @lucene.experimental * @lucene.experimental
**/ **/
final class LatLonShapePolygonQuery extends LatLonShapeQuery { final class LatLonShapePolygonQuery extends ShapeQuery {
final Polygon[] polygons; final Polygon[] polygons;
final private Polygon2D poly2D; final private Polygon2D poly2D;
@ -74,7 +79,7 @@ final class LatLonShapePolygonQuery extends LatLonShapeQuery {
@Override @Override
protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) { protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) {
LatLonShape.decodeTriangle(t, scratchTriangle); ShapeField.decodeTriangle(t, scratchTriangle);
double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle[0]); double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle[0]);
double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle[1]); double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle[1]);

View File

@ -0,0 +1,307 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
/**
* A base shape utility class used for both LatLon (spherical) and XY (cartesian) shape fields.
* <p>
* {@link Polygon}'s and {@link Line}'s are decomposed into a triangular mesh using the {@link Tessellator} utility class.
* Each {@link Triangle} is encoded by this base class and indexed as a seven dimension multi-value field.
* <p>
* Finding all shapes that intersect a range (e.g., bounding box), or target shape, at search time is efficient.
* <p>
* This class defines the static methods for encoding the three vertices of a tessellated triangles as a seven dimension point.
* The coordinates are converted from double precision values into 32 bit integers so they are sortable at index time.
* <p>
*
* @lucene.experimental
*/
public final class ShapeField {
/** vertex coordinates are encoded as 4 byte integers */
static final int BYTES = Integer.BYTES;
/** tessellated triangles are seven dimensions; the first four are the bounding box index dimensions */
protected static final FieldType TYPE = new FieldType();
static {
TYPE.setDimensions(7, 4, BYTES);
TYPE.freeze();
}
// no instance:
private ShapeField() {
}
/** polygons are decomposed into tessellated triangles using {@link org.apache.lucene.geo.Tessellator}
* these triangles are encoded and inserted as separate indexed POINT fields
*/
public static class Triangle extends Field {
Triangle(String name, int aXencoded, int aYencoded, int bXencoded, int bYencoded, int cXencoded, int cYencoded) {
super(name, TYPE);
setTriangleValue(aXencoded, aYencoded, bXencoded, bYencoded, cXencoded, cYencoded);
}
Triangle(String name, Tessellator.Triangle t) {
super(name, TYPE);
setTriangleValue(t.getEncodedX(0), t.getEncodedY(0), t.getEncodedX(1), t.getEncodedY(1), t.getEncodedX(2), t.getEncodedY(2));
}
/** sets the vertices of the triangle as integer encoded values */
protected void setTriangleValue(int aX, int aY, int bX, int bY, int cX, int cY) {
final byte[] bytes;
if (fieldsData == null) {
bytes = new byte[7 * BYTES];
fieldsData = new BytesRef(bytes);
} else {
bytes = ((BytesRef) fieldsData).bytes;
}
encodeTriangle(bytes, aY, aX, bY, bX, cY, cX);
}
}
/** Query Relation Types **/
public enum QueryRelation {
INTERSECTS, WITHIN, DISJOINT
}
private static final int MINY_MINX_MAXY_MAXX_Y_X = 0;
private static final int MINY_MINX_Y_X_MAXY_MAXX = 1;
private static final int MAXY_MINX_Y_X_MINY_MAXX = 2;
private static final int MAXY_MINX_MINY_MAXX_Y_X = 3;
private static final int Y_MINX_MINY_X_MAXY_MAXX = 4;
private static final int Y_MINX_MINY_MAXX_MAXY_X = 5;
private static final int MAXY_MINX_MINY_X_Y_MAXX = 6;
private static final int MINY_MINX_Y_MAXX_MAXY_X = 7;
/**
* A triangle is encoded using 6 points and an extra point with encoded information in three bits of how to reconstruct it.
* Triangles are encoded with CCW orientation and might be rotated to limit the number of possible reconstructions to 2^3.
* Reconstruction always happens from west to east.
*/
public static void encodeTriangle(byte[] bytes, int aLat, int aLon, int bLat, int bLon, int cLat, int cLon) {
assert bytes.length == 7 * BYTES;
int aX;
int bX;
int cX;
int aY;
int bY;
int cY;
//change orientation if CW
if (GeoUtils.orient(aLon, aLat, bLon, bLat, cLon, cLat) == -1) {
aX = cLon;
bX = bLon;
cX = aLon;
aY = cLat;
bY = bLat;
cY = aLat;
} else {
aX = aLon;
bX = bLon;
cX = cLon;
aY = aLat;
bY = bLat;
cY = cLat;
}
//rotate edges and place minX at the beginning
if (bX < aX || cX < aX) {
if (bX < cX) {
int tempX = aX;
int tempY = aY;
aX = bX;
aY = bY;
bX = cX;
bY = cY;
cX = tempX;
cY = tempY;
} else if (cX < aX) {
int tempX = aX;
int tempY = aY;
aX = cX;
aY = cY;
cX = bX;
cY = bY;
bX = tempX;
bY = tempY;
}
} else if (aX == bX && aX == cX) {
//degenerated case, all points with same longitude
//we need to prevent that aX is in the middle (not part of the MBS)
if (bY < aY || cY < aY) {
if (bY < cY) {
int tempX = aX;
int tempY = aY;
aX = bX;
aY = bY;
bX = cX;
bY = cY;
cX = tempX;
cY = tempY;
} else if (cY < aY) {
int tempX = aX;
int tempY = aY;
aX = cX;
aY = cY;
cX = bX;
cY = bY;
bX = tempX;
bY = tempY;
}
}
}
int minX = aX;
int minY = StrictMath.min(aY, StrictMath.min(bY, cY));
int maxX = StrictMath.max(aX, StrictMath.max(bX, cX));
int maxY = StrictMath.max(aY, StrictMath.max(bY, cY));
int bits, x, y;
if (minY == aY) {
if (maxY == bY && maxX == bX) {
y = cY;
x = cX;
bits = MINY_MINX_MAXY_MAXX_Y_X;
} else if (maxY == cY && maxX == cX) {
y = bY;
x = bX;
bits = MINY_MINX_Y_X_MAXY_MAXX;
} else {
y = bY;
x = cX;
bits = MINY_MINX_Y_MAXX_MAXY_X;
}
} else if (maxY == aY) {
if (minY == bY && maxX == bX) {
y = cY;
x = cX;
bits = MAXY_MINX_MINY_MAXX_Y_X;
} else if (minY == cY && maxX == cX) {
y = bY;
x = bX;
bits = MAXY_MINX_Y_X_MINY_MAXX;
} else {
y = cY;
x = bX;
bits = MAXY_MINX_MINY_X_Y_MAXX;
}
} else if (maxX == bX && minY == bY) {
y = aY;
x = cX;
bits = Y_MINX_MINY_MAXX_MAXY_X;
} else if (maxX == cX && maxY == cY) {
y = aY;
x = bX;
bits = Y_MINX_MINY_X_MAXY_MAXX;
} else {
throw new IllegalArgumentException("Could not encode the provided triangle");
}
NumericUtils.intToSortableBytes(minY, bytes, 0);
NumericUtils.intToSortableBytes(minX, bytes, BYTES);
NumericUtils.intToSortableBytes(maxY, bytes, 2 * BYTES);
NumericUtils.intToSortableBytes(maxX, bytes, 3 * BYTES);
NumericUtils.intToSortableBytes(y, bytes, 4 * BYTES);
NumericUtils.intToSortableBytes(x, bytes, 5 * BYTES);
NumericUtils.intToSortableBytes(bits, bytes, 6 * BYTES);
}
/**
* Decode a triangle encoded by {@link ShapeField#encodeTriangle(byte[], int, int, int, int, int, int)}.
*/
public static void decodeTriangle(byte[] t, int[] triangle) {
assert triangle.length == 6;
int bits = NumericUtils.sortableBytesToInt(t, 6 * BYTES);
//extract the first three bits
int tCode = (((1 << 3) - 1) & (bits >> 0));
switch (tCode) {
case MINY_MINX_MAXY_MAXX_Y_X:
triangle[0] = NumericUtils.sortableBytesToInt(t, 0 * BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 2 * BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 4 * BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * BYTES);
break;
case MINY_MINX_Y_X_MAXY_MAXX:
triangle[0] = NumericUtils.sortableBytesToInt(t, 0 * BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 4 * BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * BYTES);
break;
case MAXY_MINX_Y_X_MINY_MAXX:
triangle[0] = NumericUtils.sortableBytesToInt(t, 2 * BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 4 * BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 0 * BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * BYTES);
break;
case MAXY_MINX_MINY_MAXX_Y_X:
triangle[0] = NumericUtils.sortableBytesToInt(t, 2 * BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 4 * BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * BYTES);
break;
case Y_MINX_MINY_X_MAXY_MAXX:
triangle[0] = NumericUtils.sortableBytesToInt(t, 4 * BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * BYTES);
break;
case Y_MINX_MINY_MAXX_MAXY_X:
triangle[0] = NumericUtils.sortableBytesToInt(t, 4 * BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * BYTES);
break;
case MAXY_MINX_MINY_X_Y_MAXX:
triangle[0] = NumericUtils.sortableBytesToInt(t, 2 * BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 4 * BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * BYTES);
break;
case MINY_MINX_Y_MAXX_MAXY_X:
triangle[0] = NumericUtils.sortableBytesToInt(t, 0 * BYTES);
triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * BYTES);
triangle[2] = NumericUtils.sortableBytesToInt(t, 4 * BYTES);
triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * BYTES);
triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * BYTES);
triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * BYTES);
break;
default:
throw new IllegalArgumentException("Could not decode the provided triangle");
}
//Points of the decoded triangle must be co-planar or CCW oriented
assert GeoUtils.orient(triangle[1], triangle[0], triangle[3], triangle[2], triangle[5], triangle[4]) >= 0;
}
}

View File

@ -19,7 +19,7 @@ package org.apache.lucene.document;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.Objects;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.LeafReaderContext;
@ -41,23 +41,32 @@ import org.apache.lucene.util.DocIdSetBuilder;
import org.apache.lucene.util.FixedBitSet; import org.apache.lucene.util.FixedBitSet;
/** /**
* Base LatLonShape Query class providing common query logic for * Base query class for all spatial geometries: {@link LatLonShape} and {@link XYShape}.
* {@link LatLonShapeBoundingBoxQuery} and {@link LatLonShapePolygonQuery}
* *
* Note: this class implements the majority of the INTERSECTS, WITHIN, DISJOINT relation logic * <p>The field must be indexed using either {@link LatLonShape#createIndexableFields} or
* {@link XYShape#createIndexableFields} and the corresponding factory method must be used:
* <ul>
* <li>{@link LatLonShape#newBoxQuery newBoxQuery()} for matching geo shapes that have some {@link QueryRelation} with a bounding box.
* <li>{@link LatLonShape#newLineQuery newLineQuery()} for matching geo shapes that have some {@link QueryRelation} with a linestring.
* <li>{@link LatLonShape#newPolygonQuery newPolygonQuery()} for matching geo shapes that have some {@link QueryRelation} with a polygon.
* <li>{@link XYShape#newBoxQuery newBoxQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a bounding box.
* <li>{@link XYShape#newLineQuery newLineQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a linestring.
* <li>{@link XYShape#newPolygonQuery newPolygonQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a polygon.
* </ul>
* <p>
* *
* @lucene.experimental * @lucene.experimental
**/ **/
abstract class LatLonShapeQuery extends Query { abstract class ShapeQuery extends Query {
/** field name */ /** field name */
final String field; final String field;
/** query relation /** query relation
* disjoint: {@code CELL_OUTSIDE_QUERY} * disjoint: {@code CELL_OUTSIDE_QUERY}
* intersects: {@code CELL_CROSSES_QUERY}, * intersects: {@code CELL_CROSSES_QUERY},
* within: {@code CELL_WITHIN_QUERY} */ * within: {@code CELL_WITHIN_QUERY} */
final LatLonShape.QueryRelation queryRelation; final QueryRelation queryRelation;
protected LatLonShapeQuery(String field, final QueryRelation queryType) { protected ShapeQuery(String field, final QueryRelation queryType) {
if (field == null) { if (field == null) {
throw new IllegalArgumentException("field must not be null"); throw new IllegalArgumentException("field must not be null");
} }
@ -74,12 +83,12 @@ abstract class LatLonShapeQuery extends Query {
int maxXOffset, int maxYOffset, byte[] maxTriangle); int maxXOffset, int maxYOffset, byte[] maxTriangle);
/** returns true if the provided triangle matches the query */ /** returns true if the provided triangle matches the query */
protected abstract boolean queryMatches(byte[] triangle, int[] scratchTriangle, QueryRelation queryRelation); protected abstract boolean queryMatches(byte[] triangle, int[] scratchTriangle, ShapeField.QueryRelation queryRelation);
/** relates a range of triangles (internal node) to the query */ /** relates a range of triangles (internal node) to the query */
protected Relation relateRangeToQuery(byte[] minTriangle, byte[] maxTriangle, QueryRelation queryRelation) { protected Relation relateRangeToQuery(byte[] minTriangle, byte[] maxTriangle, QueryRelation queryRelation) {
// compute bounding box of internal node // compute bounding box of internal node
Relation r = relateRangeBBoxToQuery(LatLonShape.BYTES, 0, minTriangle, 3 * LatLonShape.BYTES, 2 * LatLonShape.BYTES, maxTriangle); Relation r = relateRangeBBoxToQuery(ShapeField.BYTES, 0, minTriangle, 3 * ShapeField.BYTES, 2 * ShapeField.BYTES, maxTriangle);
if (queryRelation == QueryRelation.DISJOINT) { if (queryRelation == QueryRelation.DISJOINT) {
return transposeRelation(r); return transposeRelation(r);
} }
@ -133,18 +142,18 @@ abstract class LatLonShapeQuery extends Query {
@Override @Override
public Relation compare(byte[] minTriangle, byte[] maxTriangle) { public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
return relateRangeToQuery(minTriangle, maxTriangle, QueryRelation.INTERSECTS); return relateRangeToQuery(minTriangle, maxTriangle, ShapeField.QueryRelation.INTERSECTS);
} }
}; };
} }
/** create a visitor that adds documents that match the query using a dense bitset. (Used by WITHIN, DISJOINT) */ /** create a visitor that adds documents that match the query using a dense bitset. (Used by WITHIN, DISJOINT) */
protected IntersectVisitor getDenseIntersectVisitor(FixedBitSet intersect, FixedBitSet disjoint, QueryRelation queryRelation) { protected IntersectVisitor getDenseIntersectVisitor(FixedBitSet intersect, FixedBitSet disjoint, ShapeField.QueryRelation queryRelation) {
return new IntersectVisitor() { return new IntersectVisitor() {
final int[] scratchTriangle = new int[6]; final int[] scratchTriangle = new int[6];
@Override @Override
public void visit(int docID) throws IOException { public void visit(int docID) throws IOException {
if (queryRelation == QueryRelation.DISJOINT) { if (queryRelation == ShapeField.QueryRelation.DISJOINT) {
// if DISJOINT query set the doc in the disjoint bitset // if DISJOINT query set the doc in the disjoint bitset
disjoint.set(docID); disjoint.set(docID);
} else { } else {
@ -189,25 +198,25 @@ abstract class LatLonShapeQuery extends Query {
return new RelationScorerSupplier(values, visitor, null, queryRelation) { return new RelationScorerSupplier(values, visitor, null, queryRelation) {
@Override @Override
public Scorer get(long leadCost) throws IOException { public Scorer get(long leadCost) throws IOException {
return getIntersectsScorer(LatLonShapeQuery.this, reader, weight, result, score(), scoreMode); return getIntersectsScorer(ShapeQuery.this, reader, weight, result, score(), scoreMode);
} }
}; };
} }
/** get a scorer supplier for all other queries (DISJOINT, WITHIN) */ /** get a scorer supplier for all other queries (DISJOINT, WITHIN) */
protected ScorerSupplier getScorerSupplier(LeafReader reader, PointValues values, Weight weight, ScoreMode scoreMode) throws IOException { protected ScorerSupplier getScorerSupplier(LeafReader reader, PointValues values, Weight weight, ScoreMode scoreMode) throws IOException {
if (queryRelation == QueryRelation.INTERSECTS) { if (queryRelation == ShapeField.QueryRelation.INTERSECTS) {
return getIntersectScorerSupplier(reader, values, weight, scoreMode); return getIntersectScorerSupplier(reader, values, weight, scoreMode);
} }
//For within and disjoint we need two passes to remove false positives in case of multi-shapes. //For within and disjoint we need two passes to remove false positives in case of multi-shapes.
FixedBitSet within = new FixedBitSet(reader.maxDoc()); FixedBitSet within = new FixedBitSet(reader.maxDoc());
FixedBitSet disjoint = new FixedBitSet(reader.maxDoc()); FixedBitSet disjoint = new FixedBitSet(reader.maxDoc());
IntersectVisitor withinVisitor = getDenseIntersectVisitor(within, disjoint, QueryRelation.WITHIN); IntersectVisitor withinVisitor = getDenseIntersectVisitor(within, disjoint, ShapeField.QueryRelation.WITHIN);
IntersectVisitor disjointVisitor = getDenseIntersectVisitor(within, disjoint, QueryRelation.DISJOINT); IntersectVisitor disjointVisitor = getDenseIntersectVisitor(within, disjoint, ShapeField.QueryRelation.DISJOINT);
return new RelationScorerSupplier(values, withinVisitor, disjointVisitor, queryRelation) { return new RelationScorerSupplier(values, withinVisitor, disjointVisitor, queryRelation) {
@Override @Override
public Scorer get(long leadCost) throws IOException { public Scorer get(long leadCost) throws IOException {
return getScorer(LatLonShapeQuery.this, weight, within, disjoint, score(), scoreMode); return getScorer(ShapeQuery.this, weight, within, disjoint, score(), scoreMode);
} }
}; };
} }
@ -290,7 +299,7 @@ abstract class LatLonShapeQuery extends Query {
} }
protected boolean equalsTo(Object o) { protected boolean equalsTo(Object o) {
return Objects.equals(field, ((LatLonShapeQuery)o).field) && this.queryRelation == ((LatLonShapeQuery)o).queryRelation; return Objects.equals(field, ((ShapeQuery)o).field) && this.queryRelation == ((ShapeQuery)o).queryRelation;
} }
/** transpose the relation; INSIDE becomes OUTSIDE, OUTSIDE becomes INSIDE, CROSSES remains unchanged */ /** transpose the relation; INSIDE becomes OUTSIDE, OUTSIDE becomes INSIDE, CROSSES remains unchanged */
@ -308,7 +317,7 @@ abstract class LatLonShapeQuery extends Query {
PointValues values; PointValues values;
IntersectVisitor visitor; IntersectVisitor visitor;
IntersectVisitor disjointVisitor;//it can be null IntersectVisitor disjointVisitor;//it can be null
QueryRelation queryRelation; ShapeField.QueryRelation queryRelation;
long cost = -1; long cost = -1;
RelationScorerSupplier(PointValues values, IntersectVisitor visitor, IntersectVisitor disjointVisitor, QueryRelation queryRelation) { RelationScorerSupplier(PointValues values, IntersectVisitor visitor, IntersectVisitor disjointVisitor, QueryRelation queryRelation) {
@ -319,7 +328,7 @@ abstract class LatLonShapeQuery extends Query {
} }
/** create a visitor that clears documents that do NOT match the polygon query; used with INTERSECTS */ /** create a visitor that clears documents that do NOT match the polygon query; used with INTERSECTS */
private IntersectVisitor getInverseIntersectVisitor(LatLonShapeQuery query, FixedBitSet result, int[] cost) { private IntersectVisitor getInverseIntersectVisitor(ShapeQuery query, FixedBitSet result, int[] cost) {
return new IntersectVisitor() { return new IntersectVisitor() {
int[] scratchTriangle = new int[6]; int[] scratchTriangle = new int[6];
@Override @Override
@ -354,7 +363,7 @@ abstract class LatLonShapeQuery extends Query {
} }
/** returns a Scorer for INTERSECT queries that uses a sparse bitset */ /** returns a Scorer for INTERSECT queries that uses a sparse bitset */
protected Scorer getIntersectsScorer(LatLonShapeQuery query, LeafReader reader, Weight weight, protected Scorer getIntersectsScorer(ShapeQuery query, LeafReader reader, Weight weight,
DocIdSetBuilder docIdSetBuilder, final float boost, ScoreMode scoreMode) throws IOException { DocIdSetBuilder docIdSetBuilder, final float boost, ScoreMode scoreMode) throws IOException {
if (values.getDocCount() == reader.maxDoc() if (values.getDocCount() == reader.maxDoc()
&& values.getDocCount() == values.size() && values.getDocCount() == values.size()
@ -376,17 +385,17 @@ abstract class LatLonShapeQuery extends Query {
} }
/** returns a Scorer for all other (non INTERSECT) queries */ /** returns a Scorer for all other (non INTERSECT) queries */
protected Scorer getScorer(LatLonShapeQuery query, Weight weight, protected Scorer getScorer(ShapeQuery query, Weight weight,
FixedBitSet intersect, FixedBitSet disjoint, final float boost, ScoreMode scoreMode) throws IOException { FixedBitSet intersect, FixedBitSet disjoint, final float boost, ScoreMode scoreMode) throws IOException {
values.intersect(visitor); values.intersect(visitor);
if (disjointVisitor != null) { if (disjointVisitor != null) {
values.intersect(disjointVisitor); values.intersect(disjointVisitor);
} }
DocIdSetIterator iterator; DocIdSetIterator iterator;
if (query.queryRelation == QueryRelation.DISJOINT) { if (query.queryRelation == ShapeField.QueryRelation.DISJOINT) {
disjoint.andNot(intersect); disjoint.andNot(intersect);
iterator = new BitSetIterator(disjoint, cost()); iterator = new BitSetIterator(disjoint, cost());
} else if (query.queryRelation == QueryRelation.WITHIN) { } else if (query.queryRelation == ShapeField.QueryRelation.WITHIN) {
intersect.andNot(disjoint); intersect.andNot(disjoint);
iterator = new BitSetIterator(intersect, cost()); iterator = new BitSetIterator(intersect, cost());
} else { } else {
@ -399,7 +408,7 @@ abstract class LatLonShapeQuery extends Query {
public long cost() { public long cost() {
if (cost == -1) { if (cost == -1) {
// Computing the cost may be expensive, so only do it if necessary // Computing the cost may be expensive, so only do it if necessary
if (queryRelation == QueryRelation.DISJOINT) { if (queryRelation == ShapeField.QueryRelation.DISJOINT) {
cost = values.estimatePointCount(disjointVisitor); cost = values.estimatePointCount(disjointVisitor);
} else { } else {
cost = values.estimatePointCount(visitor); cost = values.estimatePointCount(visitor);

View File

@ -0,0 +1,103 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
import org.apache.lucene.document.ShapeField.Triangle;
import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.index.PointValues; // javadoc
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.search.Query;
import static org.apache.lucene.geo.XYEncodingUtils.encode;
/**
* A cartesian shape utility class for indexing and searching geometries whose vertices are unitless x, y values.
* <p>
* This class defines six static factory methods for common indexing and search operations:
* <ul>
* <li>{@link #createIndexableFields(String, XYPolygon)} for indexing a cartesian polygon.
* <li>{@link #createIndexableFields(String, XYLine)} for indexing a cartesian linestring.
* <li>{@link #createIndexableFields(String, float, float)} for indexing a x, y cartesian point.
* <li>{@link #newBoxQuery newBoxQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a bounding box.
* <li>{@link #newBoxQuery newLineQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a linestring.
* <li>{@link #newBoxQuery newPolygonQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a polygon.
* </ul>
* <b>WARNING</b>: Like {@link LatLonPoint}, vertex values are indexed with some loss of precision from the
* original {@code double} values.
* @see PointValues
* @see LatLonDocValuesField
*
* @lucene.experimental
*/
public class XYShape {
// no instance:
private XYShape() {
}
/** create indexable fields for cartesian polygon geometry */
public static Field[] createIndexableFields(String fieldName, XYPolygon polygon) {
List<Tessellator.Triangle> tessellation = Tessellator.tessellate(polygon);
List<Triangle> fields = new ArrayList<>(tessellation.size());
for (Tessellator.Triangle t : tessellation) {
fields.add(new Triangle(fieldName, t));
}
return fields.toArray(new Field[fields.size()]);
}
/** create indexable fields for cartesian line geometry */
public static Field[] createIndexableFields(String fieldName, XYLine line) {
int numPoints = line.numPoints();
Field[] fields = new Field[numPoints - 1];
// create "flat" triangles
for (int i = 0, j = 1; j < numPoints; ++i, ++j) {
fields[i] = new Triangle(fieldName,
encode(line.getX(i)), encode(line.getY(i)),
encode(line.getX(j)), encode(line.getY(j)),
encode(line.getX(i)), encode(line.getY(i)));
}
return fields;
}
/** create indexable fields for cartesian point geometry */
public static Field[] createIndexableFields(String fieldName, float x, float y) {
return new Field[] {new Triangle(fieldName,
encode(x), encode(y), encode(x), encode(y), encode(x), encode(y))};
}
/** create a query to find all cartesian shapes that intersect a defined bounding box **/
public static Query newBoxQuery(String field, QueryRelation queryRelation, float minX, float maxX, float minY, float maxY) {
return new XYShapeBoundingBoxQuery(field, queryRelation, minX, maxX, minY, maxY);
}
/** create a query to find all cartesian shapes that intersect a provided linestring (or array of linestrings) **/
public static Query newLineQuery(String field, QueryRelation queryRelation, XYLine... lines) {
return new XYShapeLineQuery(field, queryRelation, lines);
}
/** create a query to find all cartesian shapes that intersect a provided polygon (or array of polygons) **/
public static Query newPolygonQuery(String field, QueryRelation queryRelation, XYPolygon... polygons) {
return new XYShapePolygonQuery(field, queryRelation, polygons);
}
}

View File

@ -0,0 +1,96 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.XYRectangle;
import org.apache.lucene.geo.XYRectangle2D;
import org.apache.lucene.index.PointValues;
/**
* Finds all previously indexed cartesian shapes that intersect the specified bounding box.
*
* <p>The field must be indexed using
* {@link org.apache.lucene.document.XYShape#createIndexableFields} added per document.
*
* @lucene.experimental
**/
public class XYShapeBoundingBoxQuery extends ShapeQuery {
final XYRectangle2D rectangle2D;
public XYShapeBoundingBoxQuery(String field, QueryRelation queryRelation, double minX, double maxX, double minY, double maxY) {
super(field, queryRelation);
XYRectangle rectangle = new XYRectangle(minX, maxX, minY, maxY);
this.rectangle2D = XYRectangle2D.create(rectangle);
}
@Override
protected PointValues.Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle,
int maxXOffset, int maxYOffset, byte[] maxTriangle) {
return rectangle2D.relateRangeBBox(minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
}
/** returns true if the query matches the encoded triangle */
@Override
protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) {
// decode indexed triangle
ShapeField.decodeTriangle(t, scratchTriangle);
int aY = scratchTriangle[0];
int aX = scratchTriangle[1];
int bY = scratchTriangle[2];
int bX = scratchTriangle[3];
int cY = scratchTriangle[4];
int cX = scratchTriangle[5];
if (queryRelation == QueryRelation.WITHIN) {
return rectangle2D.containsTriangle(aX, aY, bX, bY, cX, cY);
}
return rectangle2D.intersectsTriangle(aX, aY, bX, bY, cX, cY);
}
@Override
public boolean equals(Object o) {
return sameClassAs(o) && equalsTo(getClass().cast(o));
}
@Override
protected boolean equalsTo(Object o) {
return super.equalsTo(o) && rectangle2D.equals(((XYShapeBoundingBoxQuery)o).rectangle2D);
}
@Override
public int hashCode() {
int hash = super.hashCode();
hash = 31 * hash + rectangle2D.hashCode();
return hash;
}
@Override
public String toString(String field) {
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(':');
if (this.field.equals(field) == false) {
sb.append(" field=");
sb.append(this.field);
sb.append(':');
}
sb.append(rectangle2D.toString());
return sb.toString();
}
}

View File

@ -0,0 +1,131 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.util.Arrays;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.util.NumericUtils;
import static org.apache.lucene.geo.XYEncodingUtils.decode;
/**
* Finds all previously indexed cartesian shapes that intersect the specified arbitrary {@code XYLine}.
* <p>
* Note:
* <ul>
* <li>{@code QueryRelation.WITHIN} queries are not yet supported</li>
* </ul>
* <p>
* todo:
* <ul>
* <li>Add distance support for buffered queries</li>
* </ul>
* <p>The field must be indexed using
* {@link org.apache.lucene.document.XYShape#createIndexableFields} added per document.
*
* @lucene.experimental
**/
final class XYShapeLineQuery extends ShapeQuery {
final XYLine[] lines;
final private Line2D line2D;
public XYShapeLineQuery(String field, QueryRelation queryRelation, XYLine... lines) {
super(field, queryRelation);
/** line queries do not support within relations, only intersects and disjoint */
if (queryRelation == QueryRelation.WITHIN) {
throw new IllegalArgumentException("XYShapeLineQuery does not support " + QueryRelation.WITHIN + " queries");
}
if (lines == null) {
throw new IllegalArgumentException("lines must not be null");
}
if (lines.length == 0) {
throw new IllegalArgumentException("lines must not be empty");
}
for (int i = 0; i < lines.length; ++i) {
if (lines[i] == null) {
throw new IllegalArgumentException("line[" + i + "] must not be null");
} else if (lines[i].minX > lines[i].maxX) {
throw new IllegalArgumentException("XYShapeLineQuery: minX cannot be greater than maxX.");
} else if (lines[i].minY > lines[i].maxY) {
throw new IllegalArgumentException("XYShapeLineQuery: minY cannot be greater than maxY.");
}
}
this.lines = lines.clone();
this.line2D = Line2D.create(lines);
}
@Override
protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle,
int maxXOffset, int maxYOffset, byte[] maxTriangle) {
double minLat = decode(NumericUtils.sortableBytesToInt(minTriangle, minYOffset));
double minLon = decode(NumericUtils.sortableBytesToInt(minTriangle, minXOffset));
double maxLat = decode(NumericUtils.sortableBytesToInt(maxTriangle, maxYOffset));
double maxLon = decode(NumericUtils.sortableBytesToInt(maxTriangle, maxXOffset));
// check internal node against query
return line2D.relate(minLat, maxLat, minLon, maxLon);
}
@Override
protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) {
ShapeField.decodeTriangle(t, scratchTriangle);
double alat = decode(scratchTriangle[0]);
double alon = decode(scratchTriangle[1]);
double blat = decode(scratchTriangle[2]);
double blon = decode(scratchTriangle[3]);
double clat = decode(scratchTriangle[4]);
double clon = decode(scratchTriangle[5]);
if (queryRelation == QueryRelation.WITHIN) {
return line2D.relateTriangle(alon, alat, blon, blat, clon, clat) == Relation.CELL_INSIDE_QUERY;
}
// INTERSECTS
return line2D.relateTriangle(alon, alat, blon, blat, clon, clat) != Relation.CELL_OUTSIDE_QUERY;
}
@Override
public String toString(String field) {
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(':');
if (this.field.equals(field) == false) {
sb.append(" field=");
sb.append(this.field);
sb.append(':');
}
sb.append("XYLine(").append(lines[0].toGeoJSON()).append(")");
return sb.toString();
}
@Override
protected boolean equalsTo(Object o) {
return super.equalsTo(o) && Arrays.equals(lines, ((XYShapeLineQuery)o).lines);
}
@Override
public int hashCode() {
int hash = super.hashCode();
hash = 31 * hash + Arrays.hashCode(lines);
return hash;
}
}

View File

@ -0,0 +1,121 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.util.Arrays;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Polygon2D;
import org.apache.lucene.geo.XYEncodingUtils;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.geo.XYPolygon2D;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.util.NumericUtils;
/**
* Finds all previously indexed cartesian shapes that intersect the specified arbitrary cartesian {@link XYPolygon}.
*
* <p>The field must be indexed using
* {@link org.apache.lucene.document.XYShape#createIndexableFields} added per document.
*
* @lucene.experimental
**/
final class XYShapePolygonQuery extends ShapeQuery {
final XYPolygon[] polygons;
final private Polygon2D poly2D;
/**
* Creates a query that matches all indexed shapes to the provided polygons
*/
public XYShapePolygonQuery(String field, QueryRelation queryRelation, XYPolygon... polygons) {
super(field, queryRelation);
if (polygons == null) {
throw new IllegalArgumentException("polygons must not be null");
}
if (polygons.length == 0) {
throw new IllegalArgumentException("polygons must not be empty");
}
for (int i = 0; i < polygons.length; i++) {
if (polygons[i] == null) {
throw new IllegalArgumentException("polygon[" + i + "] must not be null");
} else if (polygons[i].minX > polygons[i].maxX) {
throw new IllegalArgumentException("XYShapePolygonQuery: minX cannot be greater than maxX.");
} else if (polygons[i].minY > polygons[i].maxY) {
throw new IllegalArgumentException("XYShapePolygonQuery: minY cannot be greater than maxY.");
}
}
this.polygons = polygons.clone();
this.poly2D = XYPolygon2D.create(polygons);
}
@Override
protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle,
int maxXOffset, int maxYOffset, byte[] maxTriangle) {
double minLat = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, minYOffset));
double minLon = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(minTriangle, minXOffset));
double maxLat = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(maxTriangle, maxYOffset));
double maxLon = XYEncodingUtils.decode(NumericUtils.sortableBytesToInt(maxTriangle, maxXOffset));
// check internal node against query
return poly2D.relate(minLat, maxLat, minLon, maxLon);
}
@Override
protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) {
ShapeField.decodeTriangle(t, scratchTriangle);
double alat = XYEncodingUtils.decode(scratchTriangle[0]);
double alon = XYEncodingUtils.decode(scratchTriangle[1]);
double blat = XYEncodingUtils.decode(scratchTriangle[2]);
double blon = XYEncodingUtils.decode(scratchTriangle[3]);
double clat = XYEncodingUtils.decode(scratchTriangle[4]);
double clon = XYEncodingUtils.decode(scratchTriangle[5]);
if (queryRelation == QueryRelation.WITHIN) {
return poly2D.relateTriangle(alon, alat, blon, blat, clon, clat) == Relation.CELL_INSIDE_QUERY;
}
// INTERSECTS
return poly2D.relateTriangle(alon, alat, blon, blat, clon, clat) != Relation.CELL_OUTSIDE_QUERY;
}
@Override
public String toString(String field) {
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(':');
if (this.field.equals(field) == false) {
sb.append(" field=");
sb.append(this.field);
sb.append(':');
}
sb.append("XYPolygon(").append(polygons[0].toGeoJSON()).append(")");
return sb.toString();
}
@Override
protected boolean equalsTo(Object o) {
return super.equalsTo(o) && Arrays.equals(polygons, ((XYShapePolygonQuery)o).polygons);
}
@Override
public int hashCode() {
int hash = super.hashCode();
hash = 31 * hash + Arrays.hashCode(polygons);
return hash;
}
}

View File

@ -129,9 +129,9 @@ public class Line {
sb.append("LINE("); sb.append("LINE(");
for (int i = 0; i < lats.length; i++) { for (int i = 0; i < lats.length; i++) {
sb.append("[") sb.append("[")
.append(lats[i])
.append(", ")
.append(lons[i]) .append(lons[i])
.append(", ")
.append(lats[i])
.append("]"); .append("]");
} }
sb.append(')'); sb.append(')');

View File

@ -21,7 +21,7 @@ import org.apache.lucene.index.PointValues.Relation;
import static org.apache.lucene.geo.GeoUtils.orient; import static org.apache.lucene.geo.GeoUtils.orient;
/** /**
* 2D line implementation represented as a balanced interval tree of edges. * 2D geo line implementation represented as a balanced interval tree of edges.
* <p> * <p>
* Line {@code Line2D} Construction takes {@code O(n log n)} time for sorting and tree construction. * Line {@code Line2D} Construction takes {@code O(n log n)} time for sorting and tree construction.
* {@link #relate relate()} are {@code O(n)}, but for most practical lines are much faster than brute force. * {@link #relate relate()} are {@code O(n)}, but for most practical lines are much faster than brute force.
@ -33,6 +33,10 @@ public final class Line2D extends EdgeTree {
super(line.minLat, line.maxLat, line.minLon, line.maxLon, line.getLats(), line.getLons()); super(line.minLat, line.maxLat, line.minLon, line.maxLon, line.getLats(), line.getLons());
} }
private Line2D(XYLine line) {
super(line.minY, line.maxY, line.minX, line.maxX, line.getY(), line.getX());
}
/** create a Line2D edge tree from provided array of Linestrings */ /** create a Line2D edge tree from provided array of Linestrings */
public static Line2D create(Line... lines) { public static Line2D create(Line... lines) {
Line2D components[] = new Line2D[lines.length]; Line2D components[] = new Line2D[lines.length];
@ -42,6 +46,15 @@ public final class Line2D extends EdgeTree {
return (Line2D)createTree(components, 0, components.length - 1, false); return (Line2D)createTree(components, 0, components.length - 1, false);
} }
/** create a Line2D edge tree from provided array of Linestrings */
public static Line2D create(XYLine... lines) {
Line2D components[] = new Line2D[lines.length];
for (int i = 0; i < components.length; ++i) {
components[i] = new Line2D(lines[i]);
}
return (Line2D)createTree(components, 0, components.length - 1, false);
}
@Override @Override
protected Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) { protected Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) {
if (tree.crossesBox(minLat, maxLat, minLon, maxLon, true)) { if (tree.crossesBox(minLat, maxLat, minLon, maxLon, true)) {

View File

@ -18,8 +18,9 @@
package org.apache.lucene.geo; package org.apache.lucene.geo;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
import org.apache.lucene.index.PointValues; import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.util.NumericUtils; import org.apache.lucene.util.NumericUtils;
import static java.lang.Integer.BYTES; import static java.lang.Integer.BYTES;
@ -32,19 +33,19 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
import static org.apache.lucene.geo.GeoUtils.orient; import static org.apache.lucene.geo.GeoUtils.orient;
/** /**
* 2D rectangle implementation containing spatial logic. * 2D rectangle implementation containing geo spatial logic.
* *
* @lucene.internal * @lucene.internal
*/ */
public class Rectangle2D { public class Rectangle2D {
final byte[] bbox; protected final byte[] bbox;
final byte[] west; private final byte[] west;
final int minX; protected final int minX;
final int maxX; protected final int maxX;
final int minY; protected final int minY;
final int maxY; protected final int maxY;
private Rectangle2D(double minLat, double maxLat, double minLon, double maxLon) { protected Rectangle2D(double minLat, double maxLat, double minLon, double maxLon) {
this.bbox = new byte[4 * BYTES]; this.bbox = new byte[4 * BYTES];
int minXenc = encodeLongitudeCeil(minLon); int minXenc = encodeLongitudeCeil(minLon);
int maxXenc = encodeLongitude(maxLon); int maxXenc = encodeLongitude(maxLon);
@ -76,6 +77,16 @@ public class Rectangle2D {
} }
} }
protected Rectangle2D(int minX, int maxX, int minY, int maxY) {
this.bbox = new byte[4 * BYTES];
this.west = null;
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
encode(this.minX, this.maxX, this.minY, this.maxY, bbox);
}
/** Builds a Rectangle2D from rectangle */ /** Builds a Rectangle2D from rectangle */
public static Rectangle2D create(Rectangle rectangle) { public static Rectangle2D create(Rectangle rectangle) {
return new Rectangle2D(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); return new Rectangle2D(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon);
@ -95,10 +106,10 @@ public class Rectangle2D {
} }
/** compare this to a provided rangle bounding box **/ /** compare this to a provided rangle bounding box **/
public PointValues.Relation relateRangeBBox(int minXOffset, int minYOffset, byte[] minTriangle, public Relation relateRangeBBox(int minXOffset, int minYOffset, byte[] minTriangle,
int maxXOffset, int maxYOffset, byte[] maxTriangle) { int maxXOffset, int maxYOffset, byte[] maxTriangle) {
PointValues.Relation eastRelation = compareBBoxToRangeBBox(this.bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle); Relation eastRelation = compareBBoxToRangeBBox(this.bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
if (this.crossesDateline() && eastRelation == PointValues.Relation.CELL_OUTSIDE_QUERY) { if (this.crossesDateline() && eastRelation == Relation.CELL_OUTSIDE_QUERY) {
return compareBBoxToRangeBBox(this.west, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle); return compareBBoxToRangeBBox(this.west, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle);
} }
return eastRelation; return eastRelation;
@ -155,24 +166,24 @@ public class Rectangle2D {
} }
/** static utility method to compare a bbox with a range of triangles (just the bbox of the triangle collection) */ /** static utility method to compare a bbox with a range of triangles (just the bbox of the triangle collection) */
private static PointValues.Relation compareBBoxToRangeBBox(final byte[] bbox, private static Relation compareBBoxToRangeBBox(final byte[] bbox,
int minXOffset, int minYOffset, byte[] minTriangle, int minXOffset, int minYOffset, byte[] minTriangle,
int maxXOffset, int maxYOffset, byte[] maxTriangle) { int maxXOffset, int maxYOffset, byte[] maxTriangle) {
// check bounding box (DISJOINT) // check bounding box (DISJOINT)
if (Arrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) > 0 || if (Arrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) > 0 ||
Arrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, BYTES, 2 * BYTES) < 0 || Arrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, BYTES, 2 * BYTES) < 0 ||
Arrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) > 0 || Arrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) > 0 ||
Arrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 0, BYTES) < 0) { Arrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 0, BYTES) < 0) {
return PointValues.Relation.CELL_OUTSIDE_QUERY; return Relation.CELL_OUTSIDE_QUERY;
} }
if (Arrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, BYTES, 2 * BYTES) >= 0 && if (Arrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, BYTES, 2 * BYTES) >= 0 &&
Arrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) <= 0 && Arrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) <= 0 &&
Arrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, BYTES) >= 0 && Arrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, BYTES) >= 0 &&
Arrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) <= 0) { Arrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) <= 0) {
return PointValues.Relation.CELL_INSIDE_QUERY; return Relation.CELL_INSIDE_QUERY;
} }
return PointValues.Relation.CELL_CROSSES_QUERY; return Relation.CELL_CROSSES_QUERY;
} }
/** /**
@ -272,4 +283,25 @@ public class Rectangle2D {
final int bMinX, final int bMaxX, final int bMinY, final int bMaxY) { final int bMinX, final int bMaxX, final int bMinY, final int bMaxY) {
return (aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY); return (aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY);
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Rectangle2D)) return false;
Rectangle2D that = (Rectangle2D) o;
return minX == that.minX &&
maxX == that.maxX &&
minY == that.minY &&
maxY == that.maxY &&
Arrays.equals(bbox, that.bbox) &&
Arrays.equals(west, that.west);
}
@Override
public int hashCode() {
int result = Objects.hash(minX, maxX, minY, maxY);
result = 31 * result + Arrays.hashCode(bbox);
result = 31 * result + Arrays.hashCode(west);
return result;
}
} }

View File

@ -80,11 +80,51 @@ final public class Tessellator {
// No Instance: // No Instance:
private Tessellator() {} private Tessellator() {}
/** Produces an array of vertices representing the triangulated result set of the Points array */
public static final List<Triangle> tessellate(final Polygon polygon) { public static final List<Triangle> tessellate(final Polygon polygon) {
// Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but this will correct); // Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but this will correct);
// then filter instances of intersections. // then filter instances of intersections.
Node outerNode = createDoublyLinkedList(polygon, 0, WindingOrder.CW); Node outerNode = createDoublyLinkedList(polygon.getPolyLons(), polygon.getPolyLats(),polygon.getWindingOrder(), true,
0, WindingOrder.CW);
// If an outer node hasn't been detected, the shape is malformed. (must comply with OGC SFA specification)
if(outerNode == null) {
throw new IllegalArgumentException("Malformed shape detected in Tessellator!");
}
// Determine if the specified list of points contains holes
if (polygon.numHoles() > 0) {
// Eliminate the hole triangulation.
outerNode = eliminateHoles(polygon, outerNode);
}
// If the shape crosses VERTEX_THRESHOLD, use z-order curve hashing:
final boolean mortonOptimized;
{
int threshold = VERTEX_THRESHOLD - polygon.numPoints();
for (int i = 0; threshold >= 0 && i < polygon.numHoles(); ++i) {
threshold -= polygon.getHole(i).numPoints();
}
// Link polygon nodes in Z-Order
mortonOptimized = threshold < 0;
if (mortonOptimized == true) {
sortByMorton(outerNode);
}
}
// Calculate the tessellation using the doubly LinkedList.
List<Triangle> result = earcutLinkedList(polygon, outerNode, new ArrayList<>(), State.INIT, mortonOptimized);
if (result.size() == 0) {
throw new IllegalArgumentException("Unable to Tessellate shape [" + polygon + "]. Possible malformed shape detected.");
}
return result;
}
public static final List<Triangle> tessellate(final XYPolygon polygon) {
// Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but this will correct);
// then filter instances of intersections.
Node outerNode = createDoublyLinkedList(polygon.getPolyX(), polygon.getPolyY(), polygon.getWindingOrder(), false,
0, WindingOrder.CW);
// If an outer node hasn't been detected, the shape is malformed. (must comply with OGC SFA specification) // If an outer node hasn't been detected, the shape is malformed. (must comply with OGC SFA specification)
if(outerNode == null) { if(outerNode == null) {
throw new IllegalArgumentException("Malformed shape detected in Tessellator!"); throw new IllegalArgumentException("Malformed shape detected in Tessellator!");
@ -120,16 +160,17 @@ final public class Tessellator {
} }
/** Creates a circular doubly linked list using polygon points. The order is governed by the specified winding order */ /** Creates a circular doubly linked list using polygon points. The order is governed by the specified winding order */
private static final Node createDoublyLinkedList(final Polygon polygon, int startIndex, final WindingOrder windingOrder) { private static final Node createDoublyLinkedList(final double[] x, final double[] y, final WindingOrder polyWindingOrder,
boolean isGeo, int startIndex, final WindingOrder windingOrder) {
Node lastNode = null; Node lastNode = null;
// Link points into the circular doubly-linked list in the specified winding order // Link points into the circular doubly-linked list in the specified winding order
if (windingOrder == polygon.getWindingOrder()) { if (windingOrder == polyWindingOrder) {
for (int i = 0; i < polygon.numPoints(); ++i) { for (int i = 0; i < x.length; ++i) {
lastNode = insertNode(polygon, startIndex++, i, lastNode); lastNode = insertNode(x, y, startIndex++, i, lastNode, isGeo);
} }
} else { } else {
for (int i = polygon.numPoints() - 1; i >= 0; --i) { for (int i = x.length - 1; i >= 0; --i) {
lastNode = insertNode(polygon, startIndex++, i, lastNode); lastNode = insertNode(x, y, startIndex++, i, lastNode, isGeo);
} }
} }
// if first and last node are the same then remove the end node and set lastNode to the start // if first and last node are the same then remove the end node and set lastNode to the start
@ -142,6 +183,29 @@ final public class Tessellator {
return filterPoints(lastNode, null); return filterPoints(lastNode, null);
} }
private static final Node eliminateHoles(final XYPolygon polygon, Node outerNode) {
// Define a list to hole a reference to each filtered hole list.
final List<Node> holeList = new ArrayList<>();
// keep a reference to the hole
final Map<Node, XYPolygon> holeListPolygons = new HashMap<>();
// Iterate through each array of hole vertices.
XYPolygon[] holes = polygon.getHoles();
int nodeIndex = polygon.numPoints() ;
for(int i = 0; i < polygon.numHoles(); ++i) {
// create the doubly-linked hole list
Node list = createDoublyLinkedList(holes[i].getPolyX(), holes[i].getPolyY(), holes[i].getWindingOrder(), false, nodeIndex, WindingOrder.CCW);
// Determine if the resulting hole polygon was successful.
if(list != null) {
// Add the leftmost vertex of the hole.
Node leftMost = fetchLeftmost(list);
holeList.add(leftMost);
holeListPolygons.put(leftMost, holes[i]);
}
nodeIndex += holes[i].numPoints();
}
return eliminateHoles(holeList, holeListPolygons, outerNode);
}
/** Links every hole into the outer loop, producing a single-ring polygon without holes. **/ /** Links every hole into the outer loop, producing a single-ring polygon without holes. **/
private static final Node eliminateHoles(final Polygon polygon, Node outerNode) { private static final Node eliminateHoles(final Polygon polygon, Node outerNode) {
// Define a list to hole a reference to each filtered hole list. // Define a list to hole a reference to each filtered hole list.
@ -153,8 +217,7 @@ final public class Tessellator {
int nodeIndex = polygon.numPoints(); int nodeIndex = polygon.numPoints();
for(int i = 0; i < polygon.numHoles(); ++i) { for(int i = 0; i < polygon.numHoles(); ++i) {
// create the doubly-linked hole list // create the doubly-linked hole list
Node list = createDoublyLinkedList(holes[i], nodeIndex, WindingOrder.CCW); Node list = createDoublyLinkedList(holes[i].getPolyLons(), holes[i].getPolyLats(), holes[i].getWindingOrder(), true, nodeIndex, WindingOrder.CCW);
if (list == list.next) { if (list == list.next) {
throw new IllegalArgumentException("Points are all coplanar in hole: " + holes[i]); throw new IllegalArgumentException("Points are all coplanar in hole: " + holes[i]);
} }
@ -167,7 +230,10 @@ final public class Tessellator {
} }
nodeIndex += holes[i].numPoints(); nodeIndex += holes[i].numPoints();
} }
return eliminateHoles(holeList, holeListPolygons, outerNode);
}
private static final Node eliminateHoles(List<Node> holeList, final Map<Node, ?> holeListPolygons, Node outerNode) {
// Sort the hole vertices by x coordinate // Sort the hole vertices by x coordinate
holeList.sort((Node pNodeA, Node pNodeB) -> holeList.sort((Node pNodeA, Node pNodeB) ->
{ {
@ -188,8 +254,22 @@ final public class Tessellator {
for(int i = 0; i < holeList.size(); ++i) { for(int i = 0; i < holeList.size(); ++i) {
// Eliminate hole triangles from the result set // Eliminate hole triangles from the result set
final Node holeNode = holeList.get(i); final Node holeNode = holeList.get(i);
final Polygon hole = holeListPolygons.get(holeNode); double holeMinX, holeMaxX, holeMinY, holeMaxY;
eliminateHole(holeNode, outerNode, hole); Object h = holeListPolygons.get(holeNode);
if (h instanceof Polygon) {
Polygon holePoly = (Polygon)h;
holeMinX = holePoly.minLon;
holeMaxX = holePoly.maxLon;
holeMinY = holePoly.minLat;
holeMaxY = holePoly.maxLat;
} else {
XYPolygon holePoly = (XYPolygon)h;
holeMinX = holePoly.minX;
holeMaxX = holePoly.maxX;
holeMinY = holePoly.minY;
holeMaxY = holePoly.maxY;
}
eliminateHole(holeNode, outerNode, holeMinX, holeMaxX, holeMinY, holeMaxY);
// Filter the new polygon. // Filter the new polygon.
outerNode = filterPoints(outerNode, outerNode.next); outerNode = filterPoints(outerNode, outerNode.next);
} }
@ -198,11 +278,11 @@ final public class Tessellator {
} }
/** Finds a bridge between vertices that connects a hole with an outer ring, and links it */ /** Finds a bridge between vertices that connects a hole with an outer ring, and links it */
private static final void eliminateHole(final Node holeNode, Node outerNode, Polygon hole) { private static final void eliminateHole(final Node holeNode, Node outerNode, double holeMinX, double holeMaxX, double holeMinY, double holeMaxY) {
// Attempt to find a common point between the HoleNode and OuterNode. // Attempt to find a common point between the HoleNode and OuterNode.
Node next = outerNode; Node next = outerNode;
do { do {
if (Rectangle.containsPoint(next.getLat(), next.getLon(), hole.minLat, hole.maxLat, hole.minLon, hole.maxLon)) { if (Rectangle.containsPoint(next.getY(), next.getX(), holeMinY, holeMaxY, holeMinX, holeMaxX)) {
Node sharedVertex = getSharedVertex(holeNode, next); Node sharedVertex = getSharedVertex(holeNode, next);
if (sharedVertex != null) { if (sharedVertex != null) {
// Split the resulting polygon. // Split the resulting polygon.
@ -319,7 +399,7 @@ final public class Tessellator {
} }
/** Main ear slicing loop which triangulates the vertices of a polygon, provided as a doubly-linked list. **/ /** Main ear slicing loop which triangulates the vertices of a polygon, provided as a doubly-linked list. **/
private static final List<Triangle> earcutLinkedList(Polygon polygon, Node currEar, final List<Triangle> tessellation, private static final List<Triangle> earcutLinkedList(Object polygon, Node currEar, final List<Triangle> tessellation,
State state, final boolean mortonOptimized) { State state, final boolean mortonOptimized) {
earcut : do { earcut : do {
if (currEar == null || currEar.previous == currEar.next) { if (currEar == null || currEar.previous == currEar.next) {
@ -479,7 +559,7 @@ final public class Tessellator {
} }
/** Attempt to split a polygon and independently triangulate each side. Return true if the polygon was splitted **/ /** Attempt to split a polygon and independently triangulate each side. Return true if the polygon was splitted **/
private static final boolean splitEarcut(Polygon polygon, final Node start, final List<Triangle> tessellation, final boolean mortonIndexed) { private static final boolean splitEarcut(Object polygon, final Node start, final List<Triangle> tessellation, final boolean mortonIndexed) {
// Search for a valid diagonal that divides the polygon into two. // Search for a valid diagonal that divides the polygon into two.
Node searchNode = start; Node searchNode = start;
Node nextNode; Node nextNode;
@ -553,7 +633,7 @@ final public class Tessellator {
double windingSum = 0; double windingSum = 0;
do { do {
// compute signed area // compute signed area
windingSum += area(next.getLon(), next.getLat(), next.next.getLon(), next.next.getLat(), end.getLon(), end.getLat()); windingSum += area(next.getX(), next.getY(), next.next.getX(), next.next.getY(), end.getX(), end.getY());
next = next.next; next = next.next;
} while (next.next != end); } while (next.next != end);
//The polygon must be CW //The polygon must be CW
@ -734,8 +814,8 @@ final public class Tessellator {
} }
/** Creates a node and optionally links it with a previous node in a circular doubly-linked list */ /** Creates a node and optionally links it with a previous node in a circular doubly-linked list */
private static final Node insertNode(final Polygon polygon, int index, int vertexIndex, final Node lastNode) { private static final Node insertNode(final double[] x, final double[] y, int index, int vertexIndex, final Node lastNode, boolean isGeo) {
final Node node = new Node(polygon, index, vertexIndex); final Node node = new Node(x, y, index, vertexIndex, isGeo);
if(lastNode == null) { if(lastNode == null) {
node.previous = node; node.previous = node;
node.previousZ = node; node.previousZ = node;
@ -822,7 +902,9 @@ final public class Tessellator {
// vertex index in the polygon // vertex index in the polygon
private final int vrtxIdx; private final int vrtxIdx;
// reference to the polygon for lat/lon values // reference to the polygon for lat/lon values
private final Polygon polygon; // private final Polygon polygon;
private final double[] polyX;
private final double[] polyY;
// encoded x value // encoded x value
private final int x; private final int x;
// encoded y value // encoded y value
@ -839,13 +921,14 @@ final public class Tessellator {
// next z node // next z node
private Node nextZ; private Node nextZ;
protected Node(final Polygon polygon, final int index, final int vertexIndex) { protected Node(final double[] x, final double[] y, final int index, final int vertexIndex, final boolean isGeo) {
this.idx = index; this.idx = index;
this.vrtxIdx = vertexIndex; this.vrtxIdx = vertexIndex;
this.polygon = polygon; this.polyX = x;
this.y = encodeLatitude(polygon.getPolyLat(vrtxIdx)); this.polyY = y;
this.x = encodeLongitude(polygon.getPolyLon(vrtxIdx)); this.y = isGeo ? encodeLatitude(polyY[vrtxIdx]) : XYEncodingUtils.encode(polyY[vrtxIdx]);
this.morton = BitUtil.interleave(x ^ 0x80000000, y ^ 0x80000000); this.x = isGeo ? encodeLongitude(polyX[vrtxIdx]) : XYEncodingUtils.encode(polyX[vrtxIdx]);
this.morton = BitUtil.interleave(this.x ^ 0x80000000, this.y ^ 0x80000000);
this.previous = null; this.previous = null;
this.next = null; this.next = null;
this.previousZ = null; this.previousZ = null;
@ -856,7 +939,8 @@ final public class Tessellator {
protected Node(Node other) { protected Node(Node other) {
this.idx = other.idx; this.idx = other.idx;
this.vrtxIdx = other.vrtxIdx; this.vrtxIdx = other.vrtxIdx;
this.polygon = other.polygon; this.polyX = other.polyX;
this.polyY = other.polyY;
this.morton = other.morton; this.morton = other.morton;
this.x = other.x; this.x = other.x;
this.y = other.y; this.y = other.y;
@ -868,22 +952,12 @@ final public class Tessellator {
/** get the x value */ /** get the x value */
public final double getX() { public final double getX() {
return polygon.getPolyLon(vrtxIdx); return polyX[vrtxIdx];
} }
/** get the y value */ /** get the y value */
public final double getY() { public final double getY() {
return polygon.getPolyLat(vrtxIdx); return polyY[vrtxIdx];
}
/** get the longitude value */
public final double getLon() {
return polygon.getPolyLon(vrtxIdx);
}
/** get the latitude value */
public final double getLat() {
return polygon.getPolyLat(vrtxIdx);
} }
@Override @Override
@ -920,22 +994,22 @@ final public class Tessellator {
return this.vertex[vertex].y; return this.vertex[vertex].y;
} }
/** get latitude value for the given vertex */ /** get y value for the given vertex */
public double getLat(int vertex) { public double getY(int vertex) {
return this.vertex[vertex].getLat(); return this.vertex[vertex].getY();
} }
/** get longitude value for the given vertex */ /** get x value for the given vertex */
public double getLon(int vertex) { public double getX(int vertex) {
return this.vertex[vertex].getLon(); return this.vertex[vertex].getX();
} }
/** utility method to compute whether the point is in the triangle */ /** utility method to compute whether the point is in the triangle */
protected boolean containsPoint(double lat, double lon) { protected boolean containsPoint(double lat, double lon) {
return pointInTriangle(lon, lat, return pointInTriangle(lon, lat,
vertex[0].getLon(), vertex[0].getLat(), vertex[0].getX(), vertex[0].getY(),
vertex[1].getLon(), vertex[1].getLat(), vertex[1].getX(), vertex[1].getY(),
vertex[2].getLon(), vertex[2].getLat()); vertex[2].getX(), vertex[2].getY());
} }
/** pretty print the triangle vertices */ /** pretty print the triangle vertices */

View File

@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.geo;
import org.apache.lucene.util.NumericUtils;
/**
* reusable cartesian geometry encoding methods
*
* @lucene.experimental
*/
public final class XYEncodingUtils {
public static final double MIN_VAL_INCL = -Float.MAX_VALUE;
public static final double MAX_VAL_INCL = Float.MAX_VALUE;
// No instance:
private XYEncodingUtils() {
}
/** validates value is within +/-{@link Float#MAX_VALUE} coordinate bounds */
public static void checkVal(double x) {
if (Double.isNaN(x) || x < MIN_VAL_INCL || x > MAX_VAL_INCL) {
throw new IllegalArgumentException("invalid value " + x + "; must be between " + MIN_VAL_INCL + " and " + MAX_VAL_INCL);
}
}
/**
* Quantizes double (64 bit) values into 32 bits
* @param x cartesian value
* @return encoded value as a 32-bit {@code int}
* @throws IllegalArgumentException if value is out of bounds
*/
public static int encode(double x) {
checkVal(x);
return NumericUtils.floatToSortableInt((float)x);
}
/**
* Turns quantized value from {@link #encode} back into a double.
* @param encoded encoded value: 32-bit quantized value.
* @return decoded value value.
*/
public static double decode(int encoded) {
double result = NumericUtils.sortableIntToFloat(encoded);
assert result >= MIN_VAL_INCL && result <= MAX_VAL_INCL;
return result;
}
/**
* Turns quantized value from byte array back into a double.
* @param src byte array containing 4 bytes to decode at {@code offset}
* @param offset offset into {@code src} to decode from.
* @return decoded value.
*/
public static double decode(byte[] src, int offset) {
return decode(NumericUtils.sortableBytesToInt(src, offset));
}
}

View File

@ -0,0 +1,146 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.geo;
import java.util.Arrays;
/**
* Represents a line in cartesian space. You can construct the Line directly with {@code double[]}, {@code double[]} x, y arrays
* coordinates.
*
* @lucene.experimental
*/
public class XYLine {
/** array of x coordinates */
private final double[] x;
/** array of y coordinates */
private final double[] y;
/** minimum x of this line's bounding box */
public final double minX;
/** maximum x of this line's bounding box */
public final double maxX;
/** minimum y of this line's bounding box */
public final double minY;
/** maximum y of this line's bounding box */
public final double maxY;
/**
* Creates a new Line from the supplied x/y array.
*/
public XYLine(float[] x, float[] y) {
if (x == null) {
throw new IllegalArgumentException("x must not be null");
}
if (y == null) {
throw new IllegalArgumentException("y must not be null");
}
if (x.length != y.length) {
throw new IllegalArgumentException("x and y must be equal length");
}
if (x.length < 2) {
throw new IllegalArgumentException("at least 2 line points required");
}
// compute bounding box
double minX = x[0];
double minY = y[0];
double maxX = x[0];
double maxY = y[0];
for (int i = 0; i < x.length; ++i) {
minX = Math.min(x[i], minX);
minY = Math.min(y[i], minY);
maxX = Math.max(x[i], maxX);
maxY = Math.max(y[i], maxY);
}
this.x = new double[x.length];
this.y = new double[y.length];
for (int i = 0; i < x.length; ++i) {
this.x[i] = (double)x[i];
this.y[i] = (double)y[i];
}
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
}
/** returns the number of vertex points */
public int numPoints() {
return x.length;
}
/** Returns x value at given index */
public double getX(int vertex) {
return x[vertex];
}
/** Returns y value at given index */
public double getY(int vertex) {
return y[vertex];
}
/** Returns a copy of the internal x array */
public double[] getX() {
return x.clone();
}
/** Returns a copy of the internal y array */
public double[] getY() {
return y.clone();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof XYLine)) return false;
XYLine line = (XYLine) o;
return Arrays.equals(x, line.x) && Arrays.equals(y, line.y);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(x);
result = 31 * result + Arrays.hashCode(y);
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("XYLINE(");
for (int i = 0; i < x.length; i++) {
sb.append("[")
.append(x[i])
.append(", ")
.append(y[i])
.append("]");
}
sb.append(')');
return sb.toString();
}
/** prints polygons as geojson */
public String toGeoJSON() {
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(Polygon.verticesToGeoJSON(x, y));
sb.append("]");
return sb.toString();
}
}

View File

@ -0,0 +1,201 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.geo;
import java.util.Arrays;
/**
* Represents a polygon in cartesian space. You can construct the Polygon directly with {@code double[]}, {@code double[]} x, y arrays
* coordinates.
*
* @lucene.experimental
*/
public class XYPolygon {
private final double[] x;
private final double[] y;
private final XYPolygon[] holes;
/** minimum x of this polygon's bounding box area */
public final double minX;
/** maximum x of this polygon's bounding box area */
public final double maxX;
/** minimum y of this polygon's bounding box area */
public final double minY;
/** maximum y of this polygon's bounding box area */
public final double maxY;
/** winding order of the vertices */
private final GeoUtils.WindingOrder windingOrder;
/**
* Creates a new Polygon from the supplied x, y arrays, and optionally any holes.
*/
public XYPolygon(float[] x, float[] y, XYPolygon... holes) {
if (x == null) {
throw new IllegalArgumentException("x must not be null");
}
if (y == null) {
throw new IllegalArgumentException("y must not be null");
}
if (holes == null) {
throw new IllegalArgumentException("holes must not be null");
}
if (x.length != y.length) {
throw new IllegalArgumentException("x and y must be equal length");
}
if (x.length < 4) {
throw new IllegalArgumentException("at least 4 polygon points required");
}
if (x[0] != x[x.length-1]) {
throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): x[0]=" + x[0] + " x[" + (x.length-1) + "]=" + x[x.length-1]);
}
if (y[0] != y[y.length-1]) {
throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): y[0]=" + y[0] + " y[" + (y.length-1) + "]=" + y[y.length-1]);
}
for (int i = 0; i < holes.length; i++) {
XYPolygon inner = holes[i];
if (inner.holes.length > 0) {
throw new IllegalArgumentException("holes may not contain holes: polygons may not nest.");
}
}
this.x = new double[x.length];
this.y = new double[y.length];
for (int i = 0; i < x.length; ++i) {
this.x[i] = (double)x[i];
this.y[i] = (double)y[i];
}
this.holes = holes.clone();
// compute bounding box
double minX = x[0];
double maxX = x[0];
double minY = y[0];
double maxY = y[0];
double windingSum = 0d;
final int numPts = x.length - 1;
for (int i = 1, j = 0; i < numPts; j = i++) {
minX = Math.min(x[i], minX);
maxX = Math.max(x[i], maxX);
minY = Math.min(y[i], minY);
maxY = Math.max(y[i], maxY);
// compute signed area
windingSum += (x[j] - x[numPts])*(y[i] - y[numPts])
- (y[j] - y[numPts])*(x[i] - x[numPts]);
}
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
this.windingOrder = (windingSum < 0) ? GeoUtils.WindingOrder.CCW : GeoUtils.WindingOrder.CW;
}
/** returns the number of vertex points */
public int numPoints() {
return x.length;
}
/** Returns a copy of the internal x array */
public double[] getPolyX() {
return x.clone();
}
/** Returns x value at given index */
public double getPolyX(int vertex) {
return x[vertex];
}
/** Returns a copy of the internal y array */
public double[] getPolyY() {
return y.clone();
}
/** Returns y value at given index */
public double getPolyY(int vertex) {
return y[vertex];
}
/** Returns a copy of the internal holes array */
public XYPolygon[] getHoles() {
return holes.clone();
}
XYPolygon getHole(int i) {
return holes[i];
}
/** Returns the winding order (CW, COLINEAR, CCW) for the polygon shell */
public GeoUtils.WindingOrder getWindingOrder() {
return this.windingOrder;
}
/** returns the number of holes for the polygon */
public int numHoles() {
return holes.length;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(holes);
result = prime * result + Arrays.hashCode(x);
result = prime * result + Arrays.hashCode(y);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
XYPolygon other = (XYPolygon) obj;
if (!Arrays.equals(holes, other.holes)) return false;
if (!Arrays.equals(x, other.x)) return false;
if (!Arrays.equals(y, other.y)) return false;
return true;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < x.length; i++) {
sb.append("[")
.append(x[i])
.append(", ")
.append(y[i])
.append("] ");
}
if (holes.length > 0) {
sb.append(", holes=");
sb.append(Arrays.toString(holes));
}
return sb.toString();
}
/** prints polygons as geojson */
public String toGeoJSON() {
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(Polygon.verticesToGeoJSON(y, x));
for (XYPolygon hole : holes) {
sb.append(",");
sb.append(Polygon.verticesToGeoJSON(hole.y, hole.x));
}
sb.append("]");
return sb.toString();
}
}

View File

@ -0,0 +1,44 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.geo;
/**
* 2D cartesian polygon implementation represented as a balanced interval tree of edges.
*
* @lucene.internal
*/
public class XYPolygon2D extends Polygon2D {
protected XYPolygon2D(XYPolygon polygon, XYPolygon2D holes) {
super(polygon.minY, polygon.maxY, polygon.minX, polygon.maxX, polygon.getPolyY(), polygon.getPolyX(), holes);
}
/** Builds a Polygon2D from multipolygon */
public static XYPolygon2D create(XYPolygon... polygons) {
XYPolygon2D components[] = new XYPolygon2D[polygons.length];
for (int i = 0; i < components.length; i++) {
XYPolygon gon = polygons[i];
XYPolygon gonHoles[] = gon.getHoles();
XYPolygon2D holes = null;
if (gonHoles.length > 0) {
holes = create(gonHoles);
}
components[i] = new XYPolygon2D(gon, holes);
}
return (XYPolygon2D)createTree(components, 0, components.length - 1, false);
}
}

View File

@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.geo;
/** Represents a x/y cartesian rectangle. */
public class XYRectangle {
/** minimum x value */
public final double minX;
/** minimum y value */
public final double maxX;
/** maximum x value */
public final double minY;
/** maximum y value */
public final double maxY;
/** Constructs a bounding box by first validating the provided x and y coordinates */
public XYRectangle(double minX, double maxX, double minY, double maxY) {
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
assert minX <= maxX;
assert minY <= maxY;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
XYRectangle rectangle = (XYRectangle) o;
if (Double.compare(rectangle.minX, minX) != 0) return false;
if (Double.compare(rectangle.minY, minY) != 0) return false;
if (Double.compare(rectangle.maxX, maxX) != 0) return false;
return Double.compare(rectangle.maxY, maxY) == 0;
}
@Override
public int hashCode() {
int result;
long temp;
temp = Double.doubleToLongBits(minX);
result = (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(minY);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(maxX);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(maxY);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append("XYRectangle(x=");
b.append(minX);
b.append(" TO ");
b.append(maxX);
b.append(" y=");
b.append(minY);
b.append(" TO ");
b.append(maxY);
b.append(")");
return b.toString();
}
}

View File

@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.geo;
import static org.apache.lucene.geo.XYEncodingUtils.decode;
import static org.apache.lucene.geo.XYEncodingUtils.encode;
/**
* 2D rectangle implementation containing cartesian spatial logic.
*
* @lucene.internal
*/
public class XYRectangle2D extends Rectangle2D {
protected XYRectangle2D(double minX, double maxX, double minY, double maxY) {
super(encode(minX), encode(maxX), encode(minY), encode(maxY));
}
/** Builds a Rectangle2D from rectangle */
public static XYRectangle2D create(XYRectangle rectangle) {
return new XYRectangle2D(rectangle.minX, rectangle.maxX, rectangle.minY, rectangle.maxY);
}
@Override
public boolean crossesDateline() {
return false;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("XYRectangle(x=");
sb.append(decode(minX));
sb.append(" TO ");
sb.append(decode(maxX));
sb.append(" y=");
sb.append(decode(minY));
sb.append(" TO ");
sb.append(decode(maxY));
sb.append(")");
return sb.toString();
}
}

View File

@ -16,43 +16,19 @@
*/ */
package org.apache.lucene.document; package org.apache.lucene.document;
import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import com.carrotsearch.randomizedtesting.generators.RandomPicks; import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Polygon2D;
import org.apache.lucene.geo.Rectangle; import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.MultiBits;
import org.apache.lucene.index.MultiDocValues;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.index.SerialMergeScheduler;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query; import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryUtils; import org.apache.lucene.search.QueryUtils;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.SimpleCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.TestUtil;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomBoolean;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude; import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude; import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
@ -62,13 +38,8 @@ import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; import static org.apache.lucene.geo.GeoTestUtil.nextLatitude;
import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; import static org.apache.lucene.geo.GeoTestUtil.nextLongitude;
/** base test class for {@link TestLatLonLineShapeQueries}, {@link TestLatLonPointShapeQueries}, /** Base test case for testing geospatial indexing and search functionality **/
* and {@link TestLatLonPolygonShapeQueries} */ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
/** name of the LatLonShape indexed field */
protected static final String FIELD_NAME = "shape";
private static final QueryRelation[] POINT_LINE_RELATIONS = {QueryRelation.INTERSECTS, QueryRelation.DISJOINT};
protected abstract ShapeType getShapeType(); protected abstract ShapeType getShapeType();
@ -76,98 +47,75 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
return getShapeType().nextShape(); return getShapeType().nextShape();
} }
/** quantizes a latitude value to be consistent with index encoding */
protected static double quantizeLat(double rawLat) {
return decodeLatitude(encodeLatitude(rawLat));
}
/** quantizes a provided latitude value rounded up to the nearest encoded integer */
protected static double quantizeLatCeil(double rawLat) {
return decodeLatitude(encodeLatitudeCeil(rawLat));
}
/** quantizes a longitude value to be consistent with index encoding */
protected static double quantizeLon(double rawLon) {
return decodeLongitude(encodeLongitude(rawLon));
}
/** quantizes a provided longitude value rounded up to the nearest encoded integer */
protected static double quantizeLonCeil(double rawLon) {
return decodeLongitude(encodeLongitudeCeil(rawLon));
}
/** quantizes a triangle to be consistent with index encoding */
protected static double[] quantizeTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
int[] decoded = encodeDecodeTriangle(ax, ay, bx, by, cx, cy);
return new double[]{decodeLatitude(decoded[0]), decodeLongitude(decoded[1]), decodeLatitude(decoded[2]), decodeLongitude(decoded[3]), decodeLatitude(decoded[4]), decodeLongitude(decoded[5])};
}
/** encode/decode a triangle */
protected static int[] encodeDecodeTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
byte[] encoded = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(encoded, encodeLatitude(ay), encodeLongitude(ax), encodeLatitude(by), encodeLongitude(bx), encodeLatitude(cy), encodeLongitude(cx));
int[] decoded = new int[6];
LatLonShape.decodeTriangle(encoded, decoded);
return decoded;
}
/** use {@link GeoTestUtil#nextPolygon()} to create a random line; TODO: move to GeoTestUtil */
public static Line nextLine() {
Polygon poly = GeoTestUtil.nextPolygon();
double[] lats = new double[poly.numPoints() - 1];
double[] lons = new double[lats.length];
System.arraycopy(poly.getPolyLats(), 0, lats, 0, lats.length);
System.arraycopy(poly.getPolyLons(), 0, lons, 0, lons.length);
return new Line(lats, lons);
}
/**
* return a semi-random line used for queries
*
* note: shapes parameter may be used to ensure some queries intersect indexed shapes
**/
protected Line randomQueryLine(Object... shapes) {
return nextLine();
}
/** creates the array of LatLonShape.Triangle values that are used to index the shape */
protected abstract Field[] createIndexableFields(String field, Object shape);
/** adds a shape to a provided document */
private void addShapeToDoc(String field, Document doc, Object shape) {
Field[] fields = createIndexableFields(field, shape);
for (Field f : fields) {
doc.add(f);
}
}
/** factory method to create a new bounding box query */ /** factory method to create a new bounding box query */
protected Query newRectQuery(String field, QueryRelation queryRelation, double minLat, double maxLat, double minLon, double maxLon) { @Override
protected Query newRectQuery(String field, QueryRelation queryRelation, double minLon, double maxLon, double minLat, double maxLat) {
return LatLonShape.newBoxQuery(field, queryRelation, minLat, maxLat, minLon, maxLon); return LatLonShape.newBoxQuery(field, queryRelation, minLat, maxLat, minLon, maxLon);
} }
/** factory method to create a new line query */
@Override
protected Query newLineQuery(String field, QueryRelation queryRelation, Object... lines) {
return LatLonShape.newLineQuery(field, queryRelation, Arrays.stream(lines).toArray(Line[]::new));
}
/** factory method to create a new polygon query */
@Override
protected Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons) {
return LatLonShape.newPolygonQuery(field, queryRelation, Arrays.stream(polygons).toArray(Polygon[]::new));
}
@Override
protected Line2D toLine2D(Object... lines) {
return Line2D.create(Arrays.stream(lines).toArray(Line[]::new));
}
@Override
protected Polygon2D toPolygon2D(Object... polygons) {
return Polygon2D.create(Arrays.stream(polygons).toArray(Polygon[]::new));
}
@Override
public Rectangle randomQueryBox() {
return GeoTestUtil.nextBox();
}
@Override
protected double rectMinX(Object rect) {
return ((Rectangle)rect).minLon;
}
@Override
protected double rectMaxX(Object rect) {
return ((Rectangle)rect).maxLon;
}
@Override
protected double rectMinY(Object rect) {
return ((Rectangle)rect).minLat;
}
public void testBoxQueryEqualsAndHashcode() { public void testBoxQueryEqualsAndHashcode() {
Rectangle rectangle = GeoTestUtil.nextBox(); Rectangle rectangle = GeoTestUtil.nextBox();
QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values()); QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
String fieldName = "foo"; String fieldName = "foo";
Query q1 = newRectQuery(fieldName, queryRelation, rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); Query q1 = newRectQuery(fieldName, queryRelation, rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat);
Query q2 = newRectQuery(fieldName, queryRelation, rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); Query q2 = newRectQuery(fieldName, queryRelation, rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat);
QueryUtils.checkEqual(q1, q2); QueryUtils.checkEqual(q1, q2);
//different field name //different field name
Query q3 = newRectQuery("bar", queryRelation, rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); Query q3 = newRectQuery("bar", queryRelation, rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat);
QueryUtils.checkUnequal(q1, q3); QueryUtils.checkUnequal(q1, q3);
//different query relation //different query relation
QueryRelation newQueryRelation = RandomPicks.randomFrom(random(), QueryRelation.values()); QueryRelation newQueryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
Query q4 = newRectQuery(fieldName, newQueryRelation, rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); Query q4 = newRectQuery(fieldName, newQueryRelation, rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat);
if (queryRelation == newQueryRelation) { if (queryRelation == newQueryRelation) {
QueryUtils.checkEqual(q1, q4); QueryUtils.checkEqual(q1, q4);
} else { } else {
QueryUtils.checkUnequal(q1, q4); QueryUtils.checkUnequal(q1, q4);
} }
//different shape //different shape
Rectangle newRectangle = GeoTestUtil.nextBox();; Rectangle newRectangle = GeoTestUtil.nextBox();
Query q5 = newRectQuery(fieldName, queryRelation, newRectangle.minLat, newRectangle.maxLat, newRectangle.minLon, newRectangle.maxLon); Query q5 = newRectQuery(fieldName, queryRelation, rectangle.minLon, rectangle.maxLon, newRectangle.minLat, newRectangle.maxLat);
if (rectangle.equals(newRectangle)) { if (rectangle.equals(newRectangle)) {
QueryUtils.checkEqual(q1, q5); QueryUtils.checkEqual(q1, q5);
} else { } else {
@ -241,458 +189,102 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
} }
} }
// A particularly tricky adversary for BKD tree: @Override
public void testSameShapeManyTimes() throws Exception { protected double rectMaxY(Object rect) {
int numShapes = atLeast(500); return ((Rectangle)rect).maxLat;
// Every doc has 2 points:
Object theShape = nextShape();
Object[] shapes = new Object[numShapes];
Arrays.fill(shapes, theShape);
verify(shapes);
} }
// Force low cardinality leaves @Override
public void testLowCardinalityShapeManyTimes() throws Exception { protected boolean rectCrossesDateline(Object rect) {
int numShapes = atLeast(500); return ((Rectangle)rect).crossesDateline();
int cardinality = TestUtil.nextInt(random(), 2, 20);
Object[] diffShapes = new Object[cardinality];
for (int i = 0; i < cardinality; i++) {
diffShapes[i] = nextShape();
}
Object[] shapes = new Object[numShapes];
for (int i = 0; i < numShapes; i++) {
shapes[i] = diffShapes[random().nextInt(cardinality)];
}
verify(shapes);
} }
public void testRandomTiny() throws Exception { /** use {@link GeoTestUtil#nextPolygon()} to create a random line; TODO: move to GeoTestUtil */
// Make sure single-leaf-node case is OK: @Override
doTestRandom(10); public Line nextLine() {
return getNextLine();
} }
@Slow public static Line getNextLine() {
public void testRandomMedium() throws Exception { Polygon poly = GeoTestUtil.nextPolygon();
doTestRandom(1000); double[] lats = new double[poly.numPoints() - 1];
double[] lons = new double[lats.length];
System.arraycopy(poly.getPolyLats(), 0, lats, 0, lats.length);
System.arraycopy(poly.getPolyLons(), 0, lons, 0, lons.length);
return new Line(lats, lons);
} }
@Slow @Override
@Nightly protected Polygon nextPolygon() {
public void testRandomBig() throws Exception { return GeoTestUtil.nextPolygon();
doTestRandom(20000);
} }
protected void doTestRandom(int count) throws Exception { @Override
int numShapes = atLeast(count); protected Encoder getEncoder() {
ShapeType type = getShapeType(); return new Encoder() {
@Override
if (VERBOSE) { double quantizeX(double raw) {
System.out.println("TEST: number of " + type.name() + " shapes=" + numShapes); return decodeLongitude(encodeLongitude(raw));
}
Object[] shapes = new Object[numShapes];
for (int id = 0; id < numShapes; ++id) {
int x = randomIntBetween(0, 20);
if (x == 17) {
shapes[id] = null;
if (VERBOSE) {
System.out.println(" id=" + id + " is missing");
}
} else {
// create a new shape
shapes[id] = nextShape();
}
}
verify(shapes);
}
private void verify(Object... shapes) throws Exception {
IndexWriterConfig iwc = newIndexWriterConfig();
iwc.setMergeScheduler(new SerialMergeScheduler());
int mbd = iwc.getMaxBufferedDocs();
if (mbd != -1 && mbd < shapes.length / 100) {
iwc.setMaxBufferedDocs(shapes.length / 100);
}
Directory dir;
if (shapes.length > 1000) {
dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
} else {
dir = newDirectory();
}
IndexWriter w = new IndexWriter(dir, iwc);
// index random polygons
indexRandomShapes(w, shapes);
// query testing
final IndexReader reader = DirectoryReader.open(w);
// test random bbox queries
verifyRandomBBoxQueries(reader, shapes);
// test random line queries
verifyRandomLineQueries(reader, shapes);
// test random polygon queries
verifyRandomPolygonQueries(reader, shapes);
IOUtils.close(w, reader, dir);
}
protected void indexRandomShapes(IndexWriter w, Object... shapes) throws Exception {
Set<Integer> deleted = new HashSet<>();
for (int id = 0; id < shapes.length; ++id) {
Document doc = new Document();
doc.add(newStringField("id", "" + id, Field.Store.NO));
doc.add(new NumericDocValuesField("id", id));
if (shapes[id] != null) {
addShapeToDoc(FIELD_NAME, doc, shapes[id]);
}
w.addDocument(doc);
if (id > 0 && random().nextInt(100) == 42) {
int idToDelete = random().nextInt(id);
w.deleteDocuments(new Term("id", ""+idToDelete));
deleted.add(idToDelete);
if (VERBOSE) {
System.out.println(" delete id=" + idToDelete);
}
}
}
if (randomBoolean()) {
w.forceMerge(1);
}
}
/** test random generated bounding boxes */
protected void verifyRandomBBoxQueries(IndexReader reader, Object... shapes) throws Exception {
IndexSearcher s = newSearcher(reader);
final int iters = scaledIterationCount(shapes.length);
Bits liveDocs = MultiBits.getLiveDocs(s.getIndexReader());
int maxDoc = s.getIndexReader().maxDoc();
for (int iter = 0; iter < iters; ++iter) {
if (VERBOSE) {
System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
} }
// BBox @Override
Rectangle rect = GeoTestUtil.nextBox(); double quantizeXCeil(double raw) {
QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values()); return decodeLongitude(encodeLongitudeCeil(raw));
Query query = newRectQuery(FIELD_NAME, queryRelation, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
if (VERBOSE) {
System.out.println(" query=" + query + ", relation=" + queryRelation);
} }
final FixedBitSet hits = new FixedBitSet(maxDoc); @Override
s.search(query, new SimpleCollector() { double quantizeY(double raw) {
return decodeLatitude(encodeLatitude(raw));
private int docBase;
@Override
public ScoreMode scoreMode() {
return ScoreMode.COMPLETE_NO_SCORES;
}
@Override
protected void doSetNextReader(LeafReaderContext context) throws IOException {
docBase = context.docBase;
}
@Override
public void collect(int doc) throws IOException {
hits.set(docBase+doc);
}
});
boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
for (int docID = 0; docID < maxDoc; ++docID) {
assertEquals(docID, docIDToID.nextDoc());
int id = (int) docIDToID.longValue();
boolean expected;
double qMinLon = quantizeLonCeil(rect.minLon);
double qMaxLon = quantizeLon(rect.maxLon);
double qMinLat = quantizeLatCeil(rect.minLat);
double qMaxLat = quantizeLat(rect.maxLat);
if (liveDocs != null && liveDocs.get(docID) == false) {
// document is deleted
expected = false;
} else if (shapes[id] == null) {
expected = false;
} else {
// check quantized poly against quantized query
if (qMinLon > qMaxLon && rect.crossesDateline() == false) {
// if the quantization creates a false dateline crossing (because of encodeCeil):
// then do not use encodeCeil
qMinLon = quantizeLon(rect.minLon);
}
if (qMinLat > qMaxLat) {
qMinLat = quantizeLat(rect.maxLat);
}
expected = getValidator(queryRelation).testBBoxQuery(qMinLat, qMaxLat, qMinLon, qMaxLon, shapes[id]);
}
if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder();
if (expected) {
b.append("FAIL: id=" + id + " should match but did not\n");
} else {
b.append("FAIL: id=" + id + " should not match but did\n");
}
b.append(" relation=" + queryRelation + "\n");
b.append(" query=" + query + " docID=" + docID + "\n");
if (shapes[id] instanceof Object[]) {
b.append(" shape=" + Arrays.toString((Object[]) shapes[id]) + "\n");
} else {
b.append(" shape=" + shapes[id] + "\n");
}
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" rect=Rectangle(lat=" + quantizeLatCeil(rect.minLat) + " TO " + quantizeLat(rect.maxLat) + " lon=" + qMinLon + " TO " + quantizeLon(rect.maxLon) + ")\n"); if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
}
}
if (fail) {
fail("some hits were wrong");
}
}
}
private int scaledIterationCount(int shapes) {
if (shapes < 500) {
return atLeast(50);
} else if (shapes < 5000) {
return atLeast(25);
} else if (shapes < 25000) {
return atLeast(5);
} else {
return atLeast(2);
}
}
/** test random generated lines */
protected void verifyRandomLineQueries(IndexReader reader, Object... shapes) throws Exception {
IndexSearcher s = newSearcher(reader);
final int iters = scaledIterationCount(shapes.length);
Bits liveDocs = MultiBits.getLiveDocs(s.getIndexReader());
int maxDoc = s.getIndexReader().maxDoc();
for (int iter = 0; iter < iters; ++iter) {
if (VERBOSE) {
System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s);
} }
// line @Override
Line queryLine = randomQueryLine(shapes); double quantizeYCeil(double raw) {
Line2D queryLine2D = Line2D.create(queryLine); return decodeLatitude(encodeLatitudeCeil(raw));
QueryRelation queryRelation = RandomPicks.randomFrom(random(), POINT_LINE_RELATIONS);
Query query = newLineQuery(FIELD_NAME, queryRelation, queryLine);
if (VERBOSE) {
System.out.println(" query=" + query + ", relation=" + queryRelation);
} }
final FixedBitSet hits = new FixedBitSet(maxDoc); /** quantizes a latitude value to be consistent with index encoding */
s.search(query, new SimpleCollector() { protected double quantizeLat(double rawLat) {
return quantizeY(rawLat);
private int docBase;
@Override
public ScoreMode scoreMode() {
return ScoreMode.COMPLETE_NO_SCORES;
}
@Override
protected void doSetNextReader(LeafReaderContext context) throws IOException {
docBase = context.docBase;
}
@Override
public void collect(int doc) throws IOException {
hits.set(docBase+doc);
}
});
boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
for (int docID = 0; docID < maxDoc; ++docID) {
assertEquals(docID, docIDToID.nextDoc());
int id = (int) docIDToID.longValue();
boolean expected;
if (liveDocs != null && liveDocs.get(docID) == false) {
// document is deleted
expected = false;
} else if (shapes[id] == null) {
expected = false;
} else {
expected = getValidator(queryRelation).testLineQuery(queryLine2D, shapes[id]);
}
if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder();
if (expected) {
b.append("FAIL: id=" + id + " should match but did not\n");
} else {
b.append("FAIL: id=" + id + " should not match but did\n");
}
b.append(" relation=" + queryRelation + "\n");
b.append(" query=" + query + " docID=" + docID + "\n");
if (shapes[id] instanceof Object[]) {
b.append(" shape=" + Arrays.toString((Object[]) shapes[id]) + "\n");
} else {
b.append(" shape=" + shapes[id] + "\n");
}
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" queryPolygon=" + queryLine.toGeoJSON());
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
}
}
if (fail) {
fail("some hits were wrong");
}
}
}
/** test random generated polygons */
protected void verifyRandomPolygonQueries(IndexReader reader, Object... shapes) throws Exception {
IndexSearcher s = newSearcher(reader);
final int iters = scaledIterationCount(shapes.length);
Bits liveDocs = MultiBits.getLiveDocs(s.getIndexReader());
int maxDoc = s.getIndexReader().maxDoc();
for (int iter = 0; iter < iters; ++iter) {
if (VERBOSE) {
System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s);
} }
// Polygon /** quantizes a provided latitude value rounded up to the nearest encoded integer */
Polygon queryPolygon = GeoTestUtil.nextPolygon(); protected double quantizeLatCeil(double rawLat) {
Polygon2D queryPoly2D = Polygon2D.create(queryPolygon); return quantizeYCeil(rawLat);
QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
Query query = newPolygonQuery(FIELD_NAME, queryRelation, queryPolygon);
if (VERBOSE) {
System.out.println(" query=" + query + ", relation=" + queryRelation);
} }
final FixedBitSet hits = new FixedBitSet(maxDoc); /** quantizes a longitude value to be consistent with index encoding */
s.search(query, new SimpleCollector() { protected double quantizeLon(double rawLon) {
return quantizeX(rawLon);
private int docBase;
@Override
public ScoreMode scoreMode() {
return ScoreMode.COMPLETE_NO_SCORES;
}
@Override
protected void doSetNextReader(LeafReaderContext context) throws IOException {
docBase = context.docBase;
}
@Override
public void collect(int doc) throws IOException {
hits.set(docBase+doc);
}
});
boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
for (int docID = 0; docID < maxDoc; ++docID) {
assertEquals(docID, docIDToID.nextDoc());
int id = (int) docIDToID.longValue();
boolean expected;
if (liveDocs != null && liveDocs.get(docID) == false) {
// document is deleted
expected = false;
} else if (shapes[id] == null) {
expected = false;
} else {
expected = getValidator(queryRelation).testPolygonQuery(queryPoly2D, shapes[id]);
}
if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder();
if (expected) {
b.append("FAIL: id=" + id + " should match but did not\n");
} else {
b.append("FAIL: id=" + id + " should not match but did\n");
}
b.append(" relation=" + queryRelation + "\n");
b.append(" query=" + query + " docID=" + docID + "\n");
if (shapes[id] instanceof Object[]) {
b.append(" shape=" + Arrays.toString((Object[]) shapes[id]) + "\n");
} else {
b.append(" shape=" + shapes[id] + "\n");
}
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" queryPolygon=" + queryPolygon.toGeoJSON());
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
}
} }
if (fail) {
fail("some hits were wrong"); /** quantizes a provided longitude value rounded up to the nearest encoded integer */
protected double quantizeLonCeil(double rawLon) {
return quantizeXCeil(rawLon);
} }
}
}
protected abstract Validator getValidator(QueryRelation relation); @Override
double[] quantizeTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
int[] decoded = encodeDecodeTriangle(ax, ay, bx, by, cx, cy);
return new double[]{decodeLatitude(decoded[0]), decodeLongitude(decoded[1]), decodeLatitude(decoded[2]), decodeLongitude(decoded[3]), decodeLatitude(decoded[4]), decodeLongitude(decoded[5])};
}
/** internal point class for testing point shapes */ @Override
protected static class Point { int[] encodeDecodeTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
double lat; byte[] encoded = new byte[7 * ShapeField.BYTES];
double lon; ShapeField.encodeTriangle(encoded, encodeLatitude(ay), encodeLongitude(ax), encodeLatitude(by), encodeLongitude(bx), encodeLatitude(cy), encodeLongitude(cx));
int[] decoded = new int[6];
public Point(double lat, double lon) { ShapeField.decodeTriangle(encoded, decoded);
this.lat = lat; return decoded;
this.lon = lon; }
} };
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("POINT(");
sb.append(lon);
sb.append(',');
sb.append(lat);
sb.append(')');
return sb.toString();
}
} }
/** internal shape type for testing different shape types */ /** internal shape type for testing different shape types */
protected enum ShapeType { protected enum ShapeType {
POINT() { POINT() {
public Point nextShape() { public Point nextShape() {
return new Point(nextLatitude(), nextLongitude()); return new Point(nextLongitude(), nextLatitude());
} }
}, },
LINE() { LINE() {
@ -737,15 +329,25 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
} }
} }
/** validator class used to test query results against "ground truth" */ /** internal lat lon point class for testing point shapes */
protected static abstract class Validator { protected static class Point {
protected QueryRelation queryRelation = QueryRelation.INTERSECTS; double lon;
public abstract boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape); double lat;
public abstract boolean testLineQuery(Line2D line2d, Object shape);
public abstract boolean testPolygonQuery(Polygon2D poly2d, Object shape);
public void setRelation(QueryRelation relation) { public Point(double lon, double lat) {
this.queryRelation = relation; this.lon = lon;
this.lat = lat;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("POINT(");
sb.append(lon);
sb.append(',');
sb.append(lat);
sb.append(')');
return sb.toString();
} }
} }
} }

View File

@ -0,0 +1,573 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.util.Arrays;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.geo.Polygon2D;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.util.LuceneTestCase;
/** base shape encoding class for testing encoding of tessellated {@link org.apache.lucene.document.XYShape} and
* {@link LatLonShape}
*/
public abstract class BaseShapeEncodingTestCase extends LuceneTestCase {
protected abstract int encodeX(double x);
protected abstract double decodeX(int x);
protected abstract int encodeY(double y);
protected abstract double decodeY(int y);
protected abstract double nextX();
protected abstract double nextY();
protected abstract Object nextPolygon();
protected abstract Polygon2D createPolygon2D(Object polygon);
//One shared point with MBR -> MinY, MinX
public void testPolygonEncodingMinLatMinLon() {
double ay = 0.0;
double ax = 0.0;
double by = 1.0;
double blon = 2.0;
double cy = 2.0;
double cx = 1.0;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(by);
int bxEnc = encodeX(blon);
int cyEnc = encodeY(cy);
int cxEnc = encodeX(cx);
verifyEncodingPermutations(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//One shared point with MBR -> MinLat, MaxLon
public void testPolygonEncodingMinLatMaxLon() {
double ay = 1.0;
double ax = 0.0;
double by = 0.0;
double blon = 2.0;
double cy = 2.0;
double cx = 1.0;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(by);
int bxEnc = encodeX(blon);
int cyEnc = encodeY(cy);
int cxEnc = encodeX(cx);
verifyEncodingPermutations(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//One shared point with MBR -> MaxLat, MaxLon
public void testPolygonEncodingMaxLatMaxLon() {
double ay = 1.0;
double ax = 0.0;
double by = 2.0;
double blon = 2.0;
double cy = 0.0;
double cx = 1.0;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(cy);
int bxEnc = encodeX(cx);
int cyEnc = encodeY(by);
int cxEnc = encodeX(blon);
verifyEncodingPermutations(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//One shared point with MBR -> MaxLat, MinLon
public void testPolygonEncodingMaxLatMinLon() {
double ay = 2.0;
double ax = 0.0;
double by = 1.0;
double blon = 2.0;
double cy = 0.0;
double cx = 1.0;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(cy);
int bxEnc = encodeX(cx);
int cyEnc = encodeY(by);
int cxEnc = encodeX(blon);
verifyEncodingPermutations(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//Two shared point with MBR -> [MinLat, MinLon], [MaxLat, MaxLon], third point below
public void testPolygonEncodingMinLatMinLonMaxLatMaxLonBelow() {
double ay = 0.0;
double ax = 0.0;
double by = 0.25;
double blon = 0.75;
double cy = 2.0;
double cx = 2.0;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(by);
int bxEnc = encodeX(blon);
int cyEnc = encodeY(cy);
int cxEnc = encodeX(cx);
verifyEncodingPermutations(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//Two shared point with MBR -> [MinLat, MinLon], [MaxLat, MaxLon], third point above
public void testPolygonEncodingMinLatMinLonMaxLatMaxLonAbove() {
double ay = 0.0;
double ax = 0.0;
double by = 2.0;
double bx = 2.0;
double cy = 1.75;
double cx = 1.25;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(by);
int bxEnc = encodeX(bx);
int cyEnc = encodeY(cy);
int cxEnc = encodeX(cx);
verifyEncodingPermutations(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//Two shared point with MBR -> [MinLat, MaxLon], [MaxLat, MinLon], third point below
public void testPolygonEncodingMinLatMaxLonMaxLatMinLonBelow() {
double ay = 8.0;
double ax = 6.0;
double by = 6.25;
double bx = 6.75;
double cy = 6.0;
double cx = 8.0;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(by);
int bxEnc = encodeX(bx);
int cyEnc = encodeY(cy);
int cxEnc = encodeX(cx);
verifyEncodingPermutations(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//Two shared point with MBR -> [MinLat, MaxLon], [MaxLat, MinLon], third point above
public void testPolygonEncodingMinLatMaxLonMaxLatMinLonAbove() {
double ay = 2.0;
double ax = 0.0;
double by = 0.0;
double bx = 2.0;
double cy = 1.75;
double cx = 1.25;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(by);
int bxEnc = encodeX(bx);
int cyEnc = encodeY(cy);
int cxEnc = encodeX(cx);
verifyEncodingPermutations(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//all points shared with MBR
public void testPolygonEncodingAllSharedAbove() {
double ay = 0.0;
double ax = 0.0;
double by = 0.0;
double bx = 2.0;
double cy = 2.0;
double cx = 2.0;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(by);
int bxEnc = encodeX(bx);
int cyEnc = encodeY(cy);
int cxEnc = encodeX(cx);
verifyEncodingPermutations(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//all points shared with MBR
public void testPolygonEncodingAllSharedBelow() {
double ay = 2.0;
double ax = 0.0;
double by = 0.0;
double bx = 0.0;
double cy = 2.0;
double cx = 2.0;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(by);
int bxEnc = encodeX(bx);
int cyEnc = encodeY(cy);
int cxEnc = encodeX(cx);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == cyEnc);
assertTrue(encoded[5] == cxEnc);
}
//[a,b,c] == [c,a,b] == [b,c,a] == [c,b,a] == [b,a,c] == [a,c,b]
public void verifyEncodingPermutations(int ayEnc, int axEnc, int byEnc, int bxEnc, int cyEnc, int cxEnc) {
//this is only valid when points are not co-planar
assertTrue(GeoUtils.orient(ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc) != 0);
byte[] b = new byte[7 * ShapeField.BYTES];
//[a,b,c]
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encodedABC = new int[6];
ShapeField.decodeTriangle(b, encodedABC);
//[c,a,b]
ShapeField.encodeTriangle(b, cyEnc, cxEnc, ayEnc, axEnc, byEnc, bxEnc);
int[] encodedCAB = new int[6];
ShapeField.decodeTriangle(b, encodedCAB);
assertTrue(Arrays.equals(encodedABC, encodedCAB));
//[b,c,a]
ShapeField.encodeTriangle(b, byEnc, bxEnc, cyEnc, cxEnc, ayEnc, axEnc);
int[] encodedBCA = new int[6];
ShapeField.decodeTriangle(b, encodedBCA);
assertTrue(Arrays.equals(encodedABC, encodedBCA));
//[c,b,a]
ShapeField.encodeTriangle(b, cyEnc, cxEnc, byEnc, bxEnc, ayEnc, axEnc);
int[] encodedCBA= new int[6];
ShapeField.decodeTriangle(b, encodedCBA);
assertTrue(Arrays.equals(encodedABC, encodedCBA));
//[b,a,c]
ShapeField.encodeTriangle(b, byEnc, bxEnc, ayEnc, axEnc, cyEnc, cxEnc);
int[] encodedBAC= new int[6];
ShapeField.decodeTriangle(b, encodedBAC);
assertTrue(Arrays.equals(encodedABC, encodedBAC));
//[a,c,b]
ShapeField.encodeTriangle(b, ayEnc, axEnc, cyEnc, cxEnc, byEnc, bxEnc);
int[] encodedACB= new int[6];
ShapeField.decodeTriangle(b, encodedACB);
assertTrue(Arrays.equals(encodedABC, encodedACB));
}
public void testPointEncoding() {
double lat = 45.0;
double lon = 45.0;
int latEnc = encodeY(lat);
int lonEnc = encodeX(lon);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, latEnc, lonEnc, latEnc, lonEnc, latEnc, lonEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == latEnc && encoded[2] == latEnc && encoded[4] == latEnc);
assertTrue(encoded[1] == lonEnc && encoded[3] == lonEnc && encoded[5] == lonEnc);
}
public void testLineEncodingSameLat() {
double lat = 2.0;
double ax = 0.0;
double bx = 2.0;
int latEnc = encodeY(lat);
int axEnc = encodeX(ax);
int bxEnc = encodeX(bx);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, latEnc, axEnc, latEnc, bxEnc, latEnc, axEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == latEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == latEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == latEnc);
assertTrue(encoded[5] == axEnc);
ShapeField.encodeTriangle(b, latEnc, axEnc, latEnc, axEnc, latEnc, bxEnc);
encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == latEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == latEnc);
assertTrue(encoded[3] == axEnc);
assertTrue(encoded[4] == latEnc);
assertTrue(encoded[5] == bxEnc);
ShapeField.encodeTriangle(b, latEnc, bxEnc, latEnc, axEnc, latEnc, axEnc);
encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == latEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == latEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == latEnc);
assertTrue(encoded[5] == axEnc);
}
public void testLineEncodingSameLon() {
double ay = 0.0;
double by = 2.0;
double lon = 2.0;
int ayEnc = encodeY(ay);
int byEnc = encodeY(by);
int lonEnc = encodeX(lon);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, lonEnc, byEnc, lonEnc, ayEnc, lonEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == lonEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == lonEnc);
assertTrue(encoded[4] == ayEnc);
assertTrue(encoded[5] == lonEnc);
ShapeField.encodeTriangle(b, ayEnc, lonEnc, ayEnc, lonEnc, byEnc, lonEnc);
encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == lonEnc);
assertTrue(encoded[2] == ayEnc);
assertTrue(encoded[3] == lonEnc);
assertTrue(encoded[4] == byEnc);
assertTrue(encoded[5] == lonEnc);
ShapeField.encodeTriangle(b, byEnc, lonEnc, ayEnc, lonEnc, ayEnc, lonEnc);
encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == lonEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == lonEnc);
assertTrue(encoded[4] == ayEnc);
assertTrue(encoded[5] == lonEnc);
}
public void testLineEncoding() {
double ay = 0.0;
double by = 2.0;
double ax = 0.0;
double bx = 2.0;
int ayEnc = encodeY(ay);
int byEnc = encodeY(by);
int axEnc = encodeX(ax);
int bxEnc = encodeX(bx);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, ayEnc, axEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == ayEnc);
assertTrue(encoded[5] == axEnc);
ShapeField.encodeTriangle(b, ayEnc, axEnc, ayEnc, axEnc, byEnc, bxEnc);
encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == ayEnc);
assertTrue(encoded[3] == axEnc);
assertTrue(encoded[4] == byEnc);
assertTrue(encoded[5] == bxEnc);
ShapeField.encodeTriangle(b, byEnc, bxEnc, ayEnc, axEnc, ayEnc, axEnc);
encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == ayEnc);
assertTrue(encoded[1] == axEnc);
assertTrue(encoded[2] == byEnc);
assertTrue(encoded[3] == bxEnc);
assertTrue(encoded[4] == ayEnc);
assertTrue(encoded[5] == axEnc);
}
public void testRandomPointEncoding() {
double ay = nextY();
double ax = nextX();
verifyEncoding(ay, ax, ay, ax, ay, ax);
}
public void testRandomLineEncoding() {
double ay = nextY();
double ax = nextX();
double by = nextY();
double bx = nextX();
verifyEncoding(ay, ax, by, bx, ay, ax);
}
public void testRandomPolygonEncoding() {
double ay = nextY();
double ax = nextX();
double by = nextY();
double bx = nextX();
double cy = nextY();
double cx = nextX();
verifyEncoding(ay, ax, by, bx, cy, cx);
}
private void verifyEncoding(double ay, double ax, double by, double bx, double cy, double cx) {
int[] original = new int[]{
encodeY(ay),
encodeX(ax),
encodeY(by),
encodeX(bx),
encodeY(cy),
encodeX(cx)};
//quantize the triangle
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, original[0], original[1], original[2], original[3], original[4], original[5]);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
double[] encodedQuantize = new double[] {
decodeY(encoded[0]),
decodeX(encoded[1]),
decodeY(encoded[2]),
decodeX(encoded[3]),
decodeY(encoded[4]),
decodeX(encoded[5])};
int orientation = GeoUtils.orient(original[1], original[0], original[3], original[2], original[5], original[4]);
//quantize original
double[] originalQuantize;
//we need to change the orientation if CW
if (orientation == -1) {
originalQuantize = new double[] {
decodeY(original[4]),
decodeX(original[5]),
decodeY(original[2]),
decodeX(original[3]),
decodeY(original[0]),
decodeX(original[1])};
} else {
originalQuantize = new double[] {
decodeY(original[0]),
decodeX(original[1]),
decodeY(original[2]),
decodeX(original[3]),
decodeY(original[4]),
decodeX(original[5])};
}
for (int i =0; i < 100; i ++) {
Polygon2D polygon2D = createPolygon2D(nextPolygon());
PointValues.Relation originalRelation = polygon2D.relateTriangle(originalQuantize[1], originalQuantize[0], originalQuantize[3], originalQuantize[2], originalQuantize[5], originalQuantize[4]);
PointValues.Relation encodedRelation = polygon2D.relateTriangle(encodedQuantize[1], encodedQuantize[0], encodedQuantize[3], encodedQuantize[2], encodedQuantize[5], encodedQuantize[4]);
assertTrue(originalRelation == encodedRelation);
}
}
public void testDegeneratedTriangle() {
double ay = 1e-26d;
double ax = 0.0d;
double by = -1.0d;
double bx = 0.0d;
double cy = 1.0d;
double cx = 0.0d;
int ayEnc = encodeY(ay);
int axEnc = encodeX(ax);
int byEnc = encodeY(by);
int bxEnc = encodeX(bx);
int cyEnc = encodeY(cy);
int cxEnc = encodeX(cx);
byte[] b = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(b, ayEnc, axEnc, byEnc, bxEnc, cyEnc, cxEnc);
int[] encoded = new int[6];
ShapeField.decodeTriangle(b, encoded);
assertTrue(encoded[0] == byEnc);
assertTrue(encoded[1] == bxEnc);
assertTrue(encoded[2] == cyEnc);
assertTrue(encoded[3] == cxEnc);
assertTrue(encoded[4] == ayEnc);
assertTrue(encoded[5] == axEnc);
}
}

View File

@ -0,0 +1,581 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Line2D;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.MultiBits;
import org.apache.lucene.index.MultiDocValues;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.index.SerialMergeScheduler;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.SimpleCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.TestUtil;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomBoolean;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
/**
* Base test case for testing spherical and cartesian geometry indexing and search functionality
* <p>
* This class is implemented by {@link BaseXYShapeTestCase} for testing XY cartesian geometry
* and {@link BaseLatLonShapeTestCase} for testing Lat Lon geospatial geometry
**/
public abstract class BaseShapeTestCase extends LuceneTestCase {
/** name of the LatLonShape indexed field */
protected static final String FIELD_NAME = "shape";
public final Encoder ENCODER;
public final Validator VALIDATOR;
protected static final QueryRelation[] POINT_LINE_RELATIONS = {QueryRelation.INTERSECTS, QueryRelation.DISJOINT};
public BaseShapeTestCase() {
ENCODER = getEncoder();
VALIDATOR = getValidator();
}
// A particularly tricky adversary for BKD tree:
public void testSameShapeManyTimes() throws Exception {
int numShapes = atLeast(500);
// Every doc has 2 points:
Object theShape = nextShape();
Object[] shapes = new Object[numShapes];
Arrays.fill(shapes, theShape);
verify(shapes);
}
// Force low cardinality leaves
public void testLowCardinalityShapeManyTimes() throws Exception {
int numShapes = atLeast(500);
int cardinality = TestUtil.nextInt(random(), 2, 20);
Object[] diffShapes = new Object[cardinality];
for (int i = 0; i < cardinality; i++) {
diffShapes[i] = nextShape();
}
Object[] shapes = new Object[numShapes];
for (int i = 0; i < numShapes; i++) {
shapes[i] = diffShapes[random().nextInt(cardinality)];
}
verify(shapes);
}
public void testRandomTiny() throws Exception {
// Make sure single-leaf-node case is OK:
doTestRandom(10);
}
@Slow
public void testRandomMedium() throws Exception {
doTestRandom(1000);
}
@Slow
@Nightly
public void testRandomBig() throws Exception {
doTestRandom(20000);
}
protected void doTestRandom(int count) throws Exception {
int numShapes = atLeast(count);
if (VERBOSE) {
System.out.println("TEST: number of " + getShapeType() + " shapes=" + numShapes);
}
Object[] shapes = new Object[numShapes];
for (int id = 0; id < numShapes; ++id) {
int x = randomIntBetween(0, 20);
if (x == 17) {
shapes[id] = null;
if (VERBOSE) {
System.out.println(" id=" + id + " is missing");
}
} else {
// create a new shape
shapes[id] = nextShape();
}
}
verify(shapes);
}
protected abstract Object getShapeType();
protected abstract Object nextShape();
protected abstract Encoder getEncoder();
/** creates the array of LatLonShape.Triangle values that are used to index the shape */
protected abstract Field[] createIndexableFields(String field, Object shape);
/** adds a shape to a provided document */
private void addShapeToDoc(String field, Document doc, Object shape) {
Field[] fields = createIndexableFields(field, shape);
for (Field f : fields) {
doc.add(f);
}
}
/** return a semi-random line used for queries **/
protected abstract Object nextLine();
protected abstract Object nextPolygon();
protected abstract Object randomQueryBox();
protected abstract double rectMinX(Object rect);
protected abstract double rectMaxX(Object rect);
protected abstract double rectMinY(Object rect);
protected abstract double rectMaxY(Object rect);
protected abstract boolean rectCrossesDateline(Object rect);
/**
* return a semi-random line used for queries
*
* note: shapes parameter may be used to ensure some queries intersect indexed shapes
**/
protected Object randomQueryLine(Object... shapes) {
return nextLine();
}
protected Object randomQueryPolygon() {
return nextPolygon();
}
/** factory method to create a new bounding box query */
protected abstract Query newRectQuery(String field, QueryRelation queryRelation, double minX, double maxX, double minY, double maxY);
/** factory method to create a new line query */
protected abstract Query newLineQuery(String field, QueryRelation queryRelation, Object... lines);
/** factory method to create a new polygon query */
protected abstract Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons);
protected abstract Line2D toLine2D(Object... line);
protected abstract Object toPolygon2D(Object... polygon);
private void verify(Object... shapes) throws Exception {
IndexWriterConfig iwc = newIndexWriterConfig();
iwc.setMergeScheduler(new SerialMergeScheduler());
int mbd = iwc.getMaxBufferedDocs();
if (mbd != -1 && mbd < shapes.length / 100) {
iwc.setMaxBufferedDocs(shapes.length / 100);
}
Directory dir;
if (shapes.length > 1000) {
dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
} else {
dir = newDirectory();
}
IndexWriter w = new IndexWriter(dir, iwc);
// index random polygons
indexRandomShapes(w, shapes);
// query testing
final IndexReader reader = DirectoryReader.open(w);
// test random bbox queries
verifyRandomQueries(reader, shapes);
IOUtils.close(w, reader, dir);
}
protected void indexRandomShapes(IndexWriter w, Object... shapes) throws Exception {
Set<Integer> deleted = new HashSet<>();
for (int id = 0; id < shapes.length; ++id) {
Document doc = new Document();
doc.add(newStringField("id", "" + id, Field.Store.NO));
doc.add(new NumericDocValuesField("id", id));
if (shapes[id] != null) {
addShapeToDoc(FIELD_NAME, doc, shapes[id]);
}
w.addDocument(doc);
if (id > 0 && random().nextInt(100) == 42) {
int idToDelete = random().nextInt(id);
w.deleteDocuments(new Term("id", ""+idToDelete));
deleted.add(idToDelete);
if (VERBOSE) {
System.out.println(" delete id=" + idToDelete);
}
}
}
if (randomBoolean()) {
w.forceMerge(1);
}
}
protected void verifyRandomQueries(IndexReader reader, Object... shapes) throws Exception {
// test random bbox queries
verifyRandomBBoxQueries(reader, shapes);
// test random line queries
verifyRandomLineQueries(reader, shapes);
// test random polygon queries
verifyRandomPolygonQueries(reader, shapes);
}
/** test random generated bounding boxes */
protected void verifyRandomBBoxQueries(IndexReader reader, Object... shapes) throws Exception {
IndexSearcher s = newSearcher(reader);
final int iters = scaledIterationCount(shapes.length);
Bits liveDocs = MultiBits.getLiveDocs(s.getIndexReader());
int maxDoc = s.getIndexReader().maxDoc();
for (int iter = 0; iter < iters; ++iter) {
if (VERBOSE) {
System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
}
// BBox
Object rect = randomQueryBox();
QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
Query query = newRectQuery(FIELD_NAME, queryRelation, rectMinX(rect), rectMaxX(rect), rectMinY(rect), rectMaxY(rect));
if (VERBOSE) {
System.out.println(" query=" + query + ", relation=" + queryRelation);
}
final FixedBitSet hits = new FixedBitSet(maxDoc);
s.search(query, new SimpleCollector() {
private int docBase;
@Override
public ScoreMode scoreMode() {
return ScoreMode.COMPLETE_NO_SCORES;
}
@Override
protected void doSetNextReader(LeafReaderContext context) throws IOException {
docBase = context.docBase;
}
@Override
public void collect(int doc) throws IOException {
hits.set(docBase+doc);
}
});
boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
for (int docID = 0; docID < maxDoc; ++docID) {
assertEquals(docID, docIDToID.nextDoc());
int id = (int) docIDToID.longValue();
boolean expected;
double qMinLon = ENCODER.quantizeXCeil(rectMinX(rect));
double qMaxLon = ENCODER.quantizeX(rectMaxX(rect));
double qMinLat = ENCODER.quantizeYCeil(rectMinY(rect));
double qMaxLat = ENCODER.quantizeY(rectMaxY(rect));
if (liveDocs != null && liveDocs.get(docID) == false) {
// document is deleted
expected = false;
} else if (shapes[id] == null) {
expected = false;
} else {
// check quantized poly against quantized query
if (qMinLon > qMaxLon && rectCrossesDateline(rect) == false) {
// if the quantization creates a false dateline crossing (because of encodeCeil):
// then do not use encodeCeil
qMinLon = ENCODER.quantizeX(rectMinX(rect));
}
if (qMinLat > qMaxLat) {
qMinLat = ENCODER.quantizeY(rectMaxY(rect));
}
expected = VALIDATOR.setRelation(queryRelation).testBBoxQuery(qMinLat, qMaxLat, qMinLon, qMaxLon, shapes[id]);
}
if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder();
if (expected) {
b.append("FAIL: id=" + id + " should match but did not\n");
} else {
b.append("FAIL: id=" + id + " should not match but did\n");
}
b.append(" relation=" + queryRelation + "\n");
b.append(" query=" + query + " docID=" + docID + "\n");
if (shapes[id] instanceof Object[]) {
b.append(" shape=" + Arrays.toString((Object[]) shapes[id]) + "\n");
} else {
b.append(" shape=" + shapes[id] + "\n");
}
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" rect=Rectangle(lat=" + ENCODER.quantizeYCeil(rectMinY(rect)) + " TO " + ENCODER.quantizeY(rectMaxY(rect)) + " lon=" + qMinLon + " TO " + ENCODER.quantizeX(rectMaxX(rect)) + ")\n");
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
}
}
if (fail) {
fail("some hits were wrong");
}
}
}
/** test random generated lines */
protected void verifyRandomLineQueries(IndexReader reader, Object... shapes) throws Exception {
IndexSearcher s = newSearcher(reader);
final int iters = scaledIterationCount(shapes.length);
Bits liveDocs = MultiBits.getLiveDocs(s.getIndexReader());
int maxDoc = s.getIndexReader().maxDoc();
for (int iter = 0; iter < iters; ++iter) {
if (VERBOSE) {
System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s);
}
// line
Object queryLine = randomQueryLine(shapes);
Line2D queryLine2D = toLine2D(queryLine);
QueryRelation queryRelation = RandomPicks.randomFrom(random(), POINT_LINE_RELATIONS);
Query query = newLineQuery(FIELD_NAME, queryRelation, queryLine);
if (VERBOSE) {
System.out.println(" query=" + query + ", relation=" + queryRelation);
}
final FixedBitSet hits = new FixedBitSet(maxDoc);
s.search(query, new SimpleCollector() {
private int docBase;
@Override
public ScoreMode scoreMode() {
return ScoreMode.COMPLETE_NO_SCORES;
}
@Override
protected void doSetNextReader(LeafReaderContext context) throws IOException {
docBase = context.docBase;
}
@Override
public void collect(int doc) throws IOException {
hits.set(docBase+doc);
}
});
boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
for (int docID = 0; docID < maxDoc; ++docID) {
assertEquals(docID, docIDToID.nextDoc());
int id = (int) docIDToID.longValue();
boolean expected;
if (liveDocs != null && liveDocs.get(docID) == false) {
// document is deleted
expected = false;
} else if (shapes[id] == null) {
expected = false;
} else {
expected = VALIDATOR.setRelation(queryRelation).testLineQuery(queryLine2D, shapes[id]);
}
if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder();
if (expected) {
b.append("FAIL: id=" + id + " should match but did not\n");
} else {
b.append("FAIL: id=" + id + " should not match but did\n");
}
b.append(" relation=" + queryRelation + "\n");
b.append(" query=" + query + " docID=" + docID + "\n");
if (shapes[id] instanceof Object[]) {
b.append(" shape=" + Arrays.toString((Object[]) shapes[id]) + "\n");
} else {
b.append(" shape=" + shapes[id] + "\n");
}
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" queryPolygon=" + queryLine);
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
}
}
if (fail) {
fail("some hits were wrong");
}
}
}
/** test random generated polygons */
protected void verifyRandomPolygonQueries(IndexReader reader, Object... shapes) throws Exception {
IndexSearcher s = newSearcher(reader);
final int iters = scaledIterationCount(shapes.length);
Bits liveDocs = MultiBits.getLiveDocs(s.getIndexReader());
int maxDoc = s.getIndexReader().maxDoc();
for (int iter = 0; iter < iters; ++iter) {
if (VERBOSE) {
System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s);
}
// Polygon
Object queryPolygon = randomQueryPolygon();
Object queryPoly2D = toPolygon2D(queryPolygon);
QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
Query query = newPolygonQuery(FIELD_NAME, queryRelation, queryPolygon);
if (VERBOSE) {
System.out.println(" query=" + query + ", relation=" + queryRelation);
}
final FixedBitSet hits = new FixedBitSet(maxDoc);
s.search(query, new SimpleCollector() {
private int docBase;
@Override
public ScoreMode scoreMode() {
return ScoreMode.COMPLETE_NO_SCORES;
}
@Override
protected void doSetNextReader(LeafReaderContext context) throws IOException {
docBase = context.docBase;
}
@Override
public void collect(int doc) throws IOException {
hits.set(docBase+doc);
}
});
boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
for (int docID = 0; docID < maxDoc; ++docID) {
assertEquals(docID, docIDToID.nextDoc());
int id = (int) docIDToID.longValue();
boolean expected;
if (liveDocs != null && liveDocs.get(docID) == false) {
// document is deleted
expected = false;
} else if (shapes[id] == null) {
expected = false;
} else {
expected = VALIDATOR.setRelation(queryRelation).testPolygonQuery(queryPoly2D, shapes[id]);
}
if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder();
if (expected) {
b.append("FAIL: id=" + id + " should match but did not\n");
} else {
b.append("FAIL: id=" + id + " should not match but did\n");
}
b.append(" relation=" + queryRelation + "\n");
b.append(" query=" + query + " docID=" + docID + "\n");
if (shapes[id] instanceof Object[]) {
b.append(" shape=" + Arrays.toString((Object[]) shapes[id]) + "\n");
} else {
b.append(" shape=" + shapes[id] + "\n");
}
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" queryPolygon=" + queryPolygon);
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
}
}
if (fail) {
fail("some hits were wrong");
}
}
}
protected abstract Validator getValidator();
protected static abstract class Encoder {
abstract double quantizeX(double raw);
abstract double quantizeXCeil(double raw);
abstract double quantizeY(double raw);
abstract double quantizeYCeil(double raw);
abstract double[] quantizeTriangle(double ax, double ay, double bx, double by, double cx, double cy);
abstract int[] encodeDecodeTriangle(double ax, double ay, double bx, double by, double cx, double cy);
}
private int scaledIterationCount(int shapes) {
if (shapes < 500) {
return atLeast(50);
} else if (shapes < 5000) {
return atLeast(25);
} else if (shapes < 25000) {
return atLeast(5);
} else {
return atLeast(2);
}
}
/** validator class used to test query results against "ground truth" */
protected static abstract class Validator {
Encoder encoder;
Validator(Encoder encoder) {
this.encoder = encoder;
}
protected QueryRelation queryRelation = QueryRelation.INTERSECTS;
public abstract boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape);
public abstract boolean testLineQuery(Line2D line2d, Object shape);
public abstract boolean testPolygonQuery(Object poly2d, Object shape);
public Validator setRelation(QueryRelation relation) {
this.queryRelation = relation;
return this;
}
}
}

View File

@ -0,0 +1,232 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.util.Arrays;
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.ShapeTestUtil;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.geo.XYPolygon2D;
import org.apache.lucene.geo.XYRectangle;
import org.apache.lucene.search.Query;
import static org.apache.lucene.geo.XYEncodingUtils.decode;
import static org.apache.lucene.geo.XYEncodingUtils.encode;
/** Base test case for testing indexing and search functionality of cartesian geometry **/
public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
protected abstract ShapeType getShapeType();
protected Object nextShape() {
return getShapeType().nextShape();
}
/** factory method to create a new bounding box query */
@Override
protected Query newRectQuery(String field, QueryRelation queryRelation, double minX, double maxX, double minY, double maxY) {
return XYShape.newBoxQuery(field, queryRelation, (float)minX, (float)maxX, (float)minY, (float)maxY);
}
/** factory method to create a new line query */
@Override
protected Query newLineQuery(String field, QueryRelation queryRelation, Object... lines) {
return XYShape.newLineQuery(field, queryRelation, Arrays.stream(lines).toArray(XYLine[]::new));
}
/** factory method to create a new polygon query */
@Override
protected Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons) {
return XYShape.newPolygonQuery(field, queryRelation, Arrays.stream(polygons).toArray(XYPolygon[]::new));
}
@Override
protected Line2D toLine2D(Object... lines) {
return Line2D.create(Arrays.stream(lines).toArray(XYLine[]::new));
}
@Override
protected XYPolygon2D toPolygon2D(Object... polygons) {
return XYPolygon2D.create(Arrays.stream(polygons).toArray(XYPolygon[]::new));
}
@Override
public XYRectangle randomQueryBox() {
return ShapeTestUtil.nextBox();
}
@Override
protected double rectMinX(Object rect) {
return ((XYRectangle)rect).minX;
}
@Override
protected double rectMaxX(Object rect) {
return ((XYRectangle)rect).maxX;
}
@Override
protected double rectMinY(Object rect) {
return ((XYRectangle)rect).minY;
}
@Override
protected double rectMaxY(Object rect) {
return ((XYRectangle)rect).maxY;
}
@Override
protected boolean rectCrossesDateline(Object rect) {
return false;
}
/** use {@link ShapeTestUtil#nextPolygon()} to create a random line; TODO: move to GeoTestUtil */
@Override
public XYLine nextLine() {
return getNextLine();
}
public static XYLine getNextLine() {
XYPolygon poly = ShapeTestUtil.nextPolygon();
float[] x = new float[poly.numPoints() - 1];
float[] y = new float[x.length];
for (int i = 0; i < x.length; ++i) {
x[i] = (float) poly.getPolyX(i);
y[i] = (float) poly.getPolyY(i);
}
return new XYLine(x, y);
}
@Override
protected XYPolygon nextPolygon() {
return ShapeTestUtil.nextPolygon();
}
@Override
protected Encoder getEncoder() {
return new Encoder() {
@Override
double quantizeX(double raw) {
return decode(encode(raw));
}
@Override
double quantizeXCeil(double raw) {
return decode(encode(raw));
}
@Override
double quantizeY(double raw) {
return decode(encode(raw));
}
@Override
double quantizeYCeil(double raw) {
return decode(encode(raw));
}
@Override
double[] quantizeTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
int[] decoded = encodeDecodeTriangle(ax, ay, bx, by, cx, cy);
return new double[]{decode(decoded[0]), decode(decoded[1]), decode(decoded[2]), decode(decoded[3]), decode(decoded[4]), decode(decoded[5])};
}
@Override
int[] encodeDecodeTriangle(double ax, double ay, double bx, double by, double cx, double cy) {
byte[] encoded = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(encoded, encode(ay), encode(ax), encode(by), encode(bx), encode(cy), encode(cx));
int[] decoded = new int[6];
ShapeField.decodeTriangle(encoded, decoded);
return decoded;
}
};
}
/** internal shape type for testing different shape types */
protected enum ShapeType {
POINT() {
public Point nextShape() {
return new Point((float)random().nextDouble(), (float)random().nextDouble());
}
},
LINE() {
public XYLine nextShape() {
XYPolygon p = ShapeTestUtil.nextPolygon();
float[] x = new float[p.numPoints() - 1];
float[] y = new float[x.length];
for (int i = 0; i < x.length; ++i) {
x[i] = (float)p.getPolyX(i);
y[i] = (float)p.getPolyY(i);
}
return new XYLine(x, y);
}
},
POLYGON() {
public XYPolygon nextShape() {
return ShapeTestUtil.nextPolygon();
}
},
MIXED() {
public Object nextShape() {
return RandomPicks.randomFrom(random(), subList).nextShape();
}
};
static ShapeType[] subList;
static {
subList = new ShapeType[] {POINT, LINE, POLYGON};
}
public abstract Object nextShape();
static ShapeType fromObject(Object shape) {
if (shape instanceof Point) {
return POINT;
} else if (shape instanceof XYLine) {
return LINE;
} else if (shape instanceof XYPolygon) {
return POLYGON;
}
throw new IllegalArgumentException("invalid shape type from " + shape.toString());
}
}
/** internal point class for testing point shapes */
protected static class Point {
float x;
float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("POINT(");
sb.append(x);
sb.append(',');
sb.append(y);
sb.append(')');
return sb.toString();
}
}
}

View File

@ -18,7 +18,7 @@ package org.apache.lucene.document;
import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.EdgeTree; import org.apache.lucene.geo.EdgeTree;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
@ -28,12 +28,10 @@ import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.geo.Rectangle2D; import org.apache.lucene.geo.Rectangle2D;
import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.index.PointValues.Relation;
/** random bounding box and polygon query tests for random generated {@link Line} types */ /** random bounding box, line, and polygon query tests for random generated {@link Line} types */
@SuppressWarnings("SimpleText") @SuppressWarnings("SimpleText")
public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase { public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase {
protected final LineValidator VALIDATOR = new LineValidator();
@Override @Override
protected ShapeType getShapeType() { protected ShapeType getShapeType() {
return ShapeType.LINE; return ShapeType.LINE;
@ -71,18 +69,21 @@ public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
protected Validator getValidator(QueryRelation queryRelation) { protected Validator getValidator() {
VALIDATOR.setRelation(queryRelation); return new LineValidator(this.ENCODER);
return VALIDATOR;
} }
protected static class LineValidator extends Validator { protected static class LineValidator extends Validator {
protected LineValidator(Encoder encoder) {
super(encoder);
}
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Line line = (Line)shape; Line line = (Line)shape;
Rectangle2D rectangle2D = Rectangle2D.create(new Rectangle(minLat, maxLat, minLon, maxLon)); Rectangle2D rectangle2D = Rectangle2D.create(new Rectangle(minLat, maxLat, minLon, maxLon));
for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) { for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) {
int[] decoded = encodeDecodeTriangle(line.getLon(i), line.getLat(i), line.getLon(j), line.getLat(j), line.getLon(i), line.getLat(i)); int[] decoded = encoder.encodeDecodeTriangle(line.getLon(i), line.getLat(i), line.getLon(j), line.getLat(j), line.getLon(i), line.getLat(i));
if (queryRelation == QueryRelation.WITHIN) { if (queryRelation == QueryRelation.WITHIN) {
if (rectangle2D.containsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == false) { if (rectangle2D.containsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == false) {
return false; return false;
@ -102,14 +103,14 @@ public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
public boolean testPolygonQuery(Polygon2D poly2d, Object shape) { public boolean testPolygonQuery(Object poly2d, Object shape) {
return testLine(poly2d, (Line) shape); return testLine((Polygon2D)poly2d, (Line) shape);
} }
private boolean testLine(EdgeTree queryPoly, Line line) { private boolean testLine(EdgeTree queryPoly, Line line) {
for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) { for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) {
double[] qTriangle = quantizeTriangle(line.getLon(i), line.getLat(i), line.getLon(j), line.getLat(j), line.getLon(i), line.getLat(i)); double[] qTriangle = encoder.quantizeTriangle(line.getLon(i), line.getLat(i), line.getLon(j), line.getLat(j), line.getLon(i), line.getLat(i));
Relation r = queryPoly.relateTriangle(qTriangle[1], qTriangle[0], qTriangle[3], qTriangle[2], qTriangle[5], qTriangle[4]); Relation r = queryPoly.relateTriangle(qTriangle[1], qTriangle[0], qTriangle[3], qTriangle[2], qTriangle[5], qTriangle[4]);
if (queryRelation == QueryRelation.DISJOINT) { if (queryRelation == QueryRelation.DISJOINT) {
if (r != Relation.CELL_OUTSIDE_QUERY) return false; if (r != Relation.CELL_OUTSIDE_QUERY) return false;

View File

@ -19,17 +19,13 @@ package org.apache.lucene.document;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.Polygon2D;
/** random bounding box and polygon query tests for random indexed arrays of {@link Line} types */ /** random bounding box, line, and polygon query tests for random indexed arrays of {@link Line} types */
public class TestLatLonMultiLineShapeQueries extends BaseLatLonShapeTestCase { public class TestLatLonMultiLineShapeQueries extends BaseLatLonShapeTestCase {
protected final MultiLineValidator VALIDATOR = new MultiLineValidator();
protected final TestLatLonLineShapeQueries.LineValidator LINEVALIDATOR = new TestLatLonLineShapeQueries.LineValidator();
@Override @Override
protected ShapeType getShapeType() { protected ShapeType getShapeType() {
return ShapeType.LINE; return ShapeType.LINE;
@ -59,13 +55,24 @@ public class TestLatLonMultiLineShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
protected Validator getValidator(QueryRelation relation) { public Validator getValidator() {
VALIDATOR.setRelation(relation); return new MultiLineValidator(ENCODER);
LINEVALIDATOR.setRelation(relation);
return VALIDATOR;
} }
protected class MultiLineValidator extends Validator { protected class MultiLineValidator extends Validator {
TestLatLonLineShapeQueries.LineValidator LINEVALIDATOR;
MultiLineValidator(Encoder encoder) {
super(encoder);
LINEVALIDATOR = new TestLatLonLineShapeQueries.LineValidator(encoder);
}
@Override
public Validator setRelation(QueryRelation relation) {
super.setRelation(relation);
LINEVALIDATOR.queryRelation = relation;
return this;
}
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Line[] lines = (Line[])shape; Line[] lines = (Line[])shape;
@ -99,7 +106,7 @@ public class TestLatLonMultiLineShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
public boolean testPolygonQuery(Polygon2D query, Object shape) { public boolean testPolygonQuery(Object query, Object shape) {
Line[] lines = (Line[])shape; Line[] lines = (Line[])shape;
for (Line l : lines) { for (Line l : lines) {
boolean b = LINEVALIDATOR.testPolygonQuery(query, l); boolean b = LINEVALIDATOR.testPolygonQuery(query, l);

View File

@ -19,17 +19,12 @@ package org.apache.lucene.document;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.Polygon2D;
/** random bounding box and polygon query tests for random indexed arrays of {@code latitude, longitude} points */ /** random bounding box, line, and polygon query tests for random indexed arrays of {@code latitude, longitude} points */
public class TestLatLonMultiPointShapeQueries extends BaseLatLonShapeTestCase { public class TestLatLonMultiPointShapeQueries extends BaseLatLonShapeTestCase {
protected final MultiPointValidator VALIDATOR = new MultiPointValidator();
protected final TestLatLonPointShapeQueries.PointValidator POINTVALIDATOR = new TestLatLonPointShapeQueries.PointValidator();
@Override @Override
protected ShapeType getShapeType() { protected ShapeType getShapeType() {
return ShapeType.POINT; return ShapeType.POINT;
@ -40,7 +35,7 @@ public class TestLatLonMultiPointShapeQueries extends BaseLatLonShapeTestCase {
int n = random().nextInt(4) + 1; int n = random().nextInt(4) + 1;
Point[] points = new Point[n]; Point[] points = new Point[n];
for (int i =0; i < n; i++) { for (int i =0; i < n; i++) {
points[i] = new Point(GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude()); points[i] = (Point)ShapeType.POINT.nextShape();
} }
return points; return points;
} }
@ -59,13 +54,24 @@ public class TestLatLonMultiPointShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
protected Validator getValidator(QueryRelation relation) { public Validator getValidator() {
VALIDATOR.setRelation(relation); return new MultiPointValidator(ENCODER);
POINTVALIDATOR.setRelation(relation);
return VALIDATOR;
} }
protected class MultiPointValidator extends Validator { protected class MultiPointValidator extends Validator {
TestLatLonPointShapeQueries.PointValidator POINTVALIDATOR;
MultiPointValidator(Encoder encoder) {
super(encoder);
POINTVALIDATOR = new TestLatLonPointShapeQueries.PointValidator(encoder);
}
@Override
public Validator setRelation(QueryRelation relation) {
super.setRelation(relation);
POINTVALIDATOR.queryRelation = relation;
return this;
}
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Point[] points = (Point[]) shape; Point[] points = (Point[]) shape;
@ -99,7 +105,7 @@ public class TestLatLonMultiPointShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
public boolean testPolygonQuery(Polygon2D query, Object shape) { public boolean testPolygonQuery(Object query, Object shape) {
Point[] points = (Point[]) shape; Point[] points = (Point[]) shape;
for (Point p : points) { for (Point p : points) {
boolean b = POINTVALIDATOR.testPolygonQuery(query, p); boolean b = POINTVALIDATOR.testPolygonQuery(query, p);

View File

@ -19,18 +19,14 @@ package org.apache.lucene.document;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Polygon2D;
import org.apache.lucene.geo.Tessellator; import org.apache.lucene.geo.Tessellator;
/** random bounding box and polygon query tests for random indexed arrays of {@link Polygon} types */ /** random bounding box, line, and polygon query tests for random indexed arrays of {@link Polygon} types */
public class TestLatLonMultiPolygonShapeQueries extends BaseLatLonShapeTestCase { public class TestLatLonMultiPolygonShapeQueries extends BaseLatLonShapeTestCase {
protected final MultiPolygonValidator VALIDATOR = new MultiPolygonValidator();
protected final TestLatLonPolygonShapeQueries.PolygonValidator POLYGONVALIDATOR = new TestLatLonPolygonShapeQueries.PolygonValidator();
@Override @Override
protected ShapeType getShapeType() { protected ShapeType getShapeType() {
return ShapeType.POLYGON; return ShapeType.POLYGON;
@ -71,13 +67,24 @@ public class TestLatLonMultiPolygonShapeQueries extends BaseLatLonShapeTestCase
} }
@Override @Override
protected Validator getValidator(QueryRelation relation) { protected Validator getValidator() {
VALIDATOR.setRelation(relation); return new MultiPolygonValidator(ENCODER);
POLYGONVALIDATOR.setRelation(relation);
return VALIDATOR;
} }
protected class MultiPolygonValidator extends Validator { protected class MultiPolygonValidator extends Validator {
TestLatLonPolygonShapeQueries.PolygonValidator POLYGONVALIDATOR;
MultiPolygonValidator(Encoder encoder) {
super(encoder);
POLYGONVALIDATOR = new TestLatLonPolygonShapeQueries.PolygonValidator(encoder);
}
@Override
public Validator setRelation(QueryRelation relation) {
super.setRelation(relation);
POLYGONVALIDATOR.queryRelation = relation;
return this;
}
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Polygon[] polygons = (Polygon[])shape; Polygon[] polygons = (Polygon[])shape;
@ -111,7 +118,7 @@ public class TestLatLonMultiPolygonShapeQueries extends BaseLatLonShapeTestCase
} }
@Override @Override
public boolean testPolygonQuery(Polygon2D query, Object shape) { public boolean testPolygonQuery(Object query, Object shape) {
Polygon[] polygons = (Polygon[])shape; Polygon[] polygons = (Polygon[])shape;
for (Polygon p : polygons) { for (Polygon p : polygons) {
boolean b = POLYGONVALIDATOR.testPolygonQuery(query, p); boolean b = POLYGONVALIDATOR.testPolygonQuery(query, p);

View File

@ -17,7 +17,7 @@
package org.apache.lucene.document; package org.apache.lucene.document;
import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.EdgeTree; import org.apache.lucene.geo.EdgeTree;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
@ -25,11 +25,9 @@ import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Polygon2D;
import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.index.PointValues.Relation;
/** random bounding box and polygon query tests for random generated {@code latitude, longitude} points */ /** random bounding box, line, and polygon query tests for random generated {@code latitude, longitude} points */
public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase { public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase {
protected final PointValidator VALIDATOR = new PointValidator();
@Override @Override
protected ShapeType getShapeType() { protected ShapeType getShapeType() {
return ShapeType.POINT; return ShapeType.POINT;
@ -67,17 +65,20 @@ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
protected Validator getValidator(QueryRelation relation) { protected Validator getValidator() {
VALIDATOR.setRelation(relation); return new PointValidator(this.ENCODER);
return VALIDATOR;
} }
protected static class PointValidator extends Validator { protected static class PointValidator extends Validator {
protected PointValidator(Encoder encoder) {
super(encoder);
}
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Point p = (Point)shape; Point p = (Point)shape;
double lat = quantizeLat(p.lat); double lat = encoder.quantizeY(p.lat);
double lon = quantizeLon(p.lon); double lon = encoder.quantizeX(p.lon);
boolean isDisjoint = lat < minLat || lat > maxLat; boolean isDisjoint = lat < minLat || lat > maxLat;
isDisjoint = isDisjoint || ((minLon > maxLon) isDisjoint = isDisjoint || ((minLon > maxLon)
@ -95,13 +96,13 @@ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
public boolean testPolygonQuery(Polygon2D poly2d, Object shape) { public boolean testPolygonQuery(Object poly2d, Object shape) {
return testPoint(poly2d, (Point) shape); return testPoint((Polygon2D)poly2d, (Point) shape);
} }
private boolean testPoint(EdgeTree tree, Point p) { private boolean testPoint(EdgeTree tree, Point p) {
double lat = quantizeLat(p.lat); double lat = encoder.quantizeY(p.lat);
double lon = quantizeLon(p.lon); double lon = encoder.quantizeX(p.lon);
// for consistency w/ the query we test the point as a triangle // for consistency w/ the query we test the point as a triangle
Relation r = tree.relateTriangle(lon, lat, lon, lat, lon, lat); Relation r = tree.relateTriangle(lon, lat, lon, lat, lon, lat);
if (queryRelation == QueryRelation.WITHIN) { if (queryRelation == QueryRelation.WITHIN) {

View File

@ -18,7 +18,7 @@ package org.apache.lucene.document;
import java.util.List; import java.util.List;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.EdgeTree; import org.apache.lucene.geo.EdgeTree;
import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
@ -28,11 +28,9 @@ import org.apache.lucene.geo.Rectangle2D;
import org.apache.lucene.geo.Tessellator; import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.index.PointValues.Relation;
/** random bounding box and polygon query tests for random indexed {@link Polygon} types */ /** random bounding box, line, and polygon query tests for random indexed {@link Polygon} types */
public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase { public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase {
protected final PolygonValidator VALIDATOR = new PolygonValidator();
@Override @Override
protected ShapeType getShapeType() { protected ShapeType getShapeType() {
return ShapeType.POLYGON; return ShapeType.POLYGON;
@ -59,19 +57,22 @@ public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
protected Validator getValidator(QueryRelation relation) { protected Validator getValidator() {
VALIDATOR.setRelation(relation); return new PolygonValidator(this.ENCODER);
return VALIDATOR;
} }
protected static class PolygonValidator extends Validator { protected static class PolygonValidator extends Validator {
protected PolygonValidator(Encoder encoder) {
super(encoder);
}
@Override @Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Polygon p = (Polygon)shape; Polygon p = (Polygon)shape;
Rectangle2D rectangle2D = Rectangle2D.create(new Rectangle(minLat, maxLat, minLon, maxLon)); Rectangle2D rectangle2D = Rectangle2D.create(new Rectangle(minLat, maxLat, minLon, maxLon));
List<Tessellator.Triangle> tessellation = Tessellator.tessellate(p); List<Tessellator.Triangle> tessellation = Tessellator.tessellate(p);
for (Tessellator.Triangle t : tessellation) { for (Tessellator.Triangle t : tessellation) {
int[] decoded = encodeDecodeTriangle(t.getLon(0), t.getLat(0), t.getLon(1), t.getLat(1), t.getLon(2), t.getLat(2)); int[] decoded = encoder.encodeDecodeTriangle(t.getX(0), t.getY(0), t.getX(1), t.getY(1), t.getX(2), t.getY(2));
if (queryRelation == QueryRelation.WITHIN) { if (queryRelation == QueryRelation.WITHIN) {
if (rectangle2D.containsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == false) { if (rectangle2D.containsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == false) {
return false; return false;
@ -91,14 +92,14 @@ public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase {
} }
@Override @Override
public boolean testPolygonQuery(Polygon2D query, Object shape) { public boolean testPolygonQuery(Object query, Object shape) {
return testPolygon(query, (Polygon) shape); return testPolygon((Polygon2D)query, (Polygon) shape);
} }
private boolean testPolygon(EdgeTree tree, Polygon shape) { private boolean testPolygon(EdgeTree tree, Polygon shape) {
List<Tessellator.Triangle> tessellation = Tessellator.tessellate(shape); List<Tessellator.Triangle> tessellation = Tessellator.tessellate(shape);
for (Tessellator.Triangle t : tessellation) { for (Tessellator.Triangle t : tessellation) {
double[] qTriangle = quantizeTriangle(t.getLon(0), t.getLat(0), t.getLon(1), t.getLat(1), t.getLon(2), t.getLat(2)); double[] qTriangle = encoder.quantizeTriangle(t.getX(0), t.getY(0), t.getX(1), t.getY(1), t.getX(2), t.getY(2));
Relation r = tree.relateTriangle(qTriangle[1], qTriangle[0], qTriangle[3], qTriangle[2], qTriangle[5], qTriangle[4]); Relation r = tree.relateTriangle(qTriangle[1], qTriangle[0], qTriangle[3], qTriangle[2], qTriangle[5], qTriangle[4]);
if (queryRelation == QueryRelation.DISJOINT) { if (queryRelation == QueryRelation.DISJOINT) {
if (r != Relation.CELL_OUTSIDE_QUERY) return false; if (r != Relation.CELL_OUTSIDE_QUERY) return false;

View File

@ -17,7 +17,7 @@
package org.apache.lucene.document; package org.apache.lucene.document;
import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line;
import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Line2D;
@ -235,11 +235,11 @@ public class TestLatLonShape extends LuceneTestCase {
Tessellator.Triangle t = Tessellator.tessellate(poly).get(0); Tessellator.Triangle t = Tessellator.tessellate(poly).get(0);
byte[] encoded = new byte[7 * LatLonShape.BYTES]; byte[] encoded = new byte[7 * ShapeField.BYTES];
LatLonShape.encodeTriangle(encoded, encodeLatitude(t.getLat(0)), encodeLongitude(t.getLon(0)), ShapeField.encodeTriangle(encoded, encodeLatitude(t.getY(0)), encodeLongitude(t.getX(0)),
encodeLatitude(t.getLat(1)), encodeLongitude(t.getLon(1)), encodeLatitude(t.getLat(2)), encodeLongitude(t.getLon(2))); encodeLatitude(t.getY(1)), encodeLongitude(t.getX(1)), encodeLatitude(t.getY(2)), encodeLongitude(t.getX(2)));
int[] decoded = new int[6]; int[] decoded = new int[6];
LatLonShape.decodeTriangle(encoded, decoded); ShapeField.decodeTriangle(encoded, decoded);
int expected =rectangle2D.intersectsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) ? 0 : 1; int expected =rectangle2D.intersectsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) ? 0 : 1;

View File

@ -16,546 +16,51 @@
*/ */
package org.apache.lucene.document; package org.apache.lucene.document;
import java.util.Arrays;
import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Polygon2D;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.util.LuceneTestCase;
/** Test case for LatLonShape encoding */ /** Test case for LatLonShape encoding */
public class TestLatLonShapeEncoding extends LuceneTestCase { public class TestLatLonShapeEncoding extends BaseShapeEncodingTestCase {
//One shared point with MBR -> MinLat, MinLon @Override
public void testPolygonEncodingMinLatMinLon() { protected int encodeX(double x) {
double alat = 0.0; return GeoEncodingUtils.encodeLongitude(x);
double alon = 0.0;
double blat = 1.0;
double blon = 2.0;
double clat = 2.0;
double clon = 1.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
int clatEnc = GeoEncodingUtils.encodeLatitude(clat);
int clonEnc = GeoEncodingUtils.encodeLongitude(clon);
verifyEncodingPermutations(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
} }
//One shared point with MBR -> MinLat, MaxLon @Override
public void testPolygonEncodingMinLatMaxLon() { protected int encodeY(double y) {
double alat = 1.0; return GeoEncodingUtils.encodeLatitude(y);
double alon = 0.0;
double blat = 0.0;
double blon = 2.0;
double clat = 2.0;
double clon = 1.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
int clatEnc = GeoEncodingUtils.encodeLatitude(clat);
int clonEnc = GeoEncodingUtils.encodeLongitude(clon);
verifyEncodingPermutations(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
} }
//One shared point with MBR -> MaxLat, MaxLon @Override
public void testPolygonEncodingMaxLatMaxLon() { protected double decodeX(int xEncoded) {
double alat = 1.0; return GeoEncodingUtils.decodeLongitude(xEncoded);
double alon = 0.0;
double blat = 2.0;
double blon = 2.0;
double clat = 0.0;
double clon = 1.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(clat);
int blonEnc = GeoEncodingUtils.encodeLongitude(clon);
int clatEnc = GeoEncodingUtils.encodeLatitude(blat);
int clonEnc = GeoEncodingUtils.encodeLongitude(blon);
verifyEncodingPermutations(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
} }
//One shared point with MBR -> MaxLat, MinLon @Override
public void testPolygonEncodingMaxLatMinLon() { protected double decodeY(int yEncoded) {
double alat = 2.0; return GeoEncodingUtils.decodeLatitude(yEncoded);
double alon = 0.0;
double blat = 1.0;
double blon = 2.0;
double clat = 0.0;
double clon = 1.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(clat);
int blonEnc = GeoEncodingUtils.encodeLongitude(clon);
int clatEnc = GeoEncodingUtils.encodeLatitude(blat);
int clonEnc = GeoEncodingUtils.encodeLongitude(blon);
verifyEncodingPermutations(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
} }
//Two shared point with MBR -> [MinLat, MinLon], [MaxLat, MaxLon], third point below @Override
public void testPolygonEncodingMinLatMinLonMaxLatMaxLonBelow() { protected double nextX() {
double alat = 0.0; return GeoTestUtil.nextLongitude();
double alon = 0.0;
double blat = 0.25;
double blon = 0.75;
double clat = 2.0;
double clon = 2.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
int clatEnc = GeoEncodingUtils.encodeLatitude(clat);
int clonEnc = GeoEncodingUtils.encodeLongitude(clon);
verifyEncodingPermutations(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
} }
//Two shared point with MBR -> [MinLat, MinLon], [MaxLat, MaxLon], third point above @Override
public void testPolygonEncodingMinLatMinLonMaxLatMaxLonAbove() { protected double nextY() {
double alat = 0.0; return GeoTestUtil.nextLatitude();
double alon = 0.0;
double blat = 2.0;
double blon = 2.0;
double clat = 1.75;
double clon = 1.25;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
int clatEnc = GeoEncodingUtils.encodeLatitude(clat);
int clonEnc = GeoEncodingUtils.encodeLongitude(clon);
verifyEncodingPermutations(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
} }
//Two shared point with MBR -> [MinLat, MaxLon], [MaxLat, MinLon], third point below @Override
public void testPolygonEncodingMinLatMaxLonMaxLatMinLonBelow() { protected Polygon nextPolygon() {
double alat = 2.0; return GeoTestUtil.nextPolygon();
double alon = 0.0;
double blat = 0.25;
double blon = 0.75;
double clat = 0.0;
double clon = 2.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
int clatEnc = GeoEncodingUtils.encodeLatitude(clat);
int clonEnc = GeoEncodingUtils.encodeLongitude(clon);
verifyEncodingPermutations(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
} }
//Two shared point with MBR -> [MinLat, MaxLon], [MaxLat, MinLon], third point above @Override
public void testPolygonEncodingMinLatMaxLonMaxLatMinLonAbove() { protected Polygon2D createPolygon2D(Object polygon) {
double alat = 2.0; return Polygon2D.create((Polygon)polygon);
double alon = 0.0;
double blat = 0.0;
double blon = 2.0;
double clat = 1.75;
double clon = 1.25;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
int clatEnc = GeoEncodingUtils.encodeLatitude(clat);
int clonEnc = GeoEncodingUtils.encodeLongitude(clon);
verifyEncodingPermutations(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
}
//all points shared with MBR
public void testPolygonEncodingAllSharedAbove() {
double alat = 0.0;
double alon = 0.0;
double blat = 0.0;
double blon = 2.0;
double clat = 2.0;
double clon = 2.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
int clatEnc = GeoEncodingUtils.encodeLatitude(clat);
int clonEnc = GeoEncodingUtils.encodeLongitude(clon);
verifyEncodingPermutations(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
}
//all points shared with MBR
public void testPolygonEncodingAllSharedBelow() {
double alat = 2.0;
double alon = 0.0;
double blat = 0.0;
double blon = 0.0;
double clat = 2.0;
double clon = 2.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
int clatEnc = GeoEncodingUtils.encodeLatitude(clat);
int clonEnc = GeoEncodingUtils.encodeLongitude(clon);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == clatEnc);
assertTrue(encoded[5] == clonEnc);
}
//[a,b,c] == [c,a,b] == [b,c,a] == [c,b,a] == [b,a,c] == [a,c,b]
public void verifyEncodingPermutations(int alatEnc, int alonEnc, int blatEnc, int blonEnc, int clatEnc, int clonEnc) {
//this is only valid when points are not co-planar
assertTrue(GeoUtils.orient(alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc) != 0);
byte[] b = new byte[7 * LatLonShape.BYTES];
//[a,b,c]
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encodedABC = new int[6];
LatLonShape.decodeTriangle(b, encodedABC);
//[c,a,b]
LatLonShape.encodeTriangle(b, clatEnc, clonEnc, alatEnc, alonEnc, blatEnc, blonEnc);
int[] encodedCAB = new int[6];
LatLonShape.decodeTriangle(b, encodedCAB);
assertTrue(Arrays.equals(encodedABC, encodedCAB));
//[b,c,a]
LatLonShape.encodeTriangle(b, blatEnc, blonEnc, clatEnc, clonEnc, alatEnc, alonEnc);
int[] encodedBCA = new int[6];
LatLonShape.decodeTriangle(b, encodedBCA);
assertTrue(Arrays.equals(encodedABC, encodedBCA));
//[c,b,a]
LatLonShape.encodeTriangle(b, clatEnc, clonEnc, blatEnc, blonEnc, alatEnc, alonEnc);
int[] encodedCBA= new int[6];
LatLonShape.decodeTriangle(b, encodedCBA);
assertTrue(Arrays.equals(encodedABC, encodedCBA));
//[b,a,c]
LatLonShape.encodeTriangle(b, blatEnc, blonEnc, alatEnc, alonEnc, clatEnc, clonEnc);
int[] encodedBAC= new int[6];
LatLonShape.decodeTriangle(b, encodedBAC);
assertTrue(Arrays.equals(encodedABC, encodedBAC));
//[a,c,b]
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, clatEnc, clonEnc, blatEnc, blonEnc);
int[] encodedACB= new int[6];
LatLonShape.decodeTriangle(b, encodedACB);
assertTrue(Arrays.equals(encodedABC, encodedACB));
}
public void testPointEncoding() {
double lat = 45.0;
double lon = 45.0;
int latEnc = GeoEncodingUtils.encodeLatitude(lat);
int lonEnc = GeoEncodingUtils.encodeLongitude(lon);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, latEnc, lonEnc, latEnc, lonEnc, latEnc, lonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == latEnc && encoded[2] == latEnc && encoded[4] == latEnc);
assertTrue(encoded[1] == lonEnc && encoded[3] == lonEnc && encoded[5] == lonEnc);
}
public void testLineEncodingSameLat() {
double lat = 2.0;
double alon = 0.0;
double blon = 2.0;
int latEnc = GeoEncodingUtils.encodeLatitude(lat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, latEnc, alonEnc, latEnc, blonEnc, latEnc, alonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == latEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == latEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == latEnc);
assertTrue(encoded[5] == alonEnc);
LatLonShape.encodeTriangle(b, latEnc, alonEnc, latEnc, alonEnc, latEnc, blonEnc);
encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == latEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == latEnc);
assertTrue(encoded[3] == alonEnc);
assertTrue(encoded[4] == latEnc);
assertTrue(encoded[5] == blonEnc);
LatLonShape.encodeTriangle(b, latEnc, blonEnc, latEnc, alonEnc, latEnc, alonEnc);
encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == latEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == latEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == latEnc);
assertTrue(encoded[5] == alonEnc);
}
public void testLineEncodingSameLon() {
double alat = 0.0;
double blat = 2.0;
double lon = 2.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int lonEnc = GeoEncodingUtils.encodeLongitude(lon);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, lonEnc, blatEnc, lonEnc, alatEnc, lonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == lonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == lonEnc);
assertTrue(encoded[4] == alatEnc);
assertTrue(encoded[5] == lonEnc);
LatLonShape.encodeTriangle(b, alatEnc, lonEnc, alatEnc, lonEnc, blatEnc, lonEnc);
encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == lonEnc);
assertTrue(encoded[2] == alatEnc);
assertTrue(encoded[3] == lonEnc);
assertTrue(encoded[4] == blatEnc);
assertTrue(encoded[5] == lonEnc);
LatLonShape.encodeTriangle(b, blatEnc, lonEnc, alatEnc, lonEnc, alatEnc, lonEnc);
encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == lonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == lonEnc);
assertTrue(encoded[4] == alatEnc);
assertTrue(encoded[5] == lonEnc);
}
public void testLineEncoding() {
double alat = 0.0;
double blat = 2.0;
double alon = 0.0;
double blon = 2.0;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, alatEnc, alonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == alatEnc);
assertTrue(encoded[5] == alonEnc);
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, alatEnc, alonEnc, blatEnc, blonEnc);
encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == alatEnc);
assertTrue(encoded[3] == alonEnc);
assertTrue(encoded[4] == blatEnc);
assertTrue(encoded[5] == blonEnc);
LatLonShape.encodeTriangle(b, blatEnc, blonEnc, alatEnc, alonEnc, alatEnc, alonEnc);
encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == alatEnc);
assertTrue(encoded[1] == alonEnc);
assertTrue(encoded[2] == blatEnc);
assertTrue(encoded[3] == blonEnc);
assertTrue(encoded[4] == alatEnc);
assertTrue(encoded[5] == alonEnc);
}
public void testRandomPointEncoding() {
double alat = GeoTestUtil.nextLatitude();
double alon = GeoTestUtil.nextLongitude();
verifyEncoding(alat, alon, alat, alon, alat, alon);
}
public void testRandomLineEncoding() {
double alat = GeoTestUtil.nextLatitude();
double alon = GeoTestUtil.nextLongitude();
double blat = GeoTestUtil.nextLatitude();
double blon = GeoTestUtil.nextLongitude();
verifyEncoding(alat, alon, blat, blon, alat, alon);
}
public void testRandomPolygonEncoding() {
double alat = GeoTestUtil.nextLatitude();
double alon = GeoTestUtil.nextLongitude();
double blat = GeoTestUtil.nextLatitude();
double blon = GeoTestUtil.nextLongitude();
double clat = GeoTestUtil.nextLatitude();
double clon = GeoTestUtil.nextLongitude();
verifyEncoding(alat, alon, blat, blon, clat, clon);
}
private void verifyEncoding(double alat, double alon, double blat, double blon, double clat, double clon) {
int[] original = new int[]{GeoEncodingUtils.encodeLatitude(alat),
GeoEncodingUtils.encodeLongitude(alon),
GeoEncodingUtils.encodeLatitude(blat),
GeoEncodingUtils.encodeLongitude(blon),
GeoEncodingUtils.encodeLatitude(clat),
GeoEncodingUtils.encodeLongitude(clon)};
//quantize the triangle
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, original[0], original[1], original[2], original[3], original[4], original[5]);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
double[] encodedQuantize = new double[] {GeoEncodingUtils.decodeLatitude(encoded[0]),
GeoEncodingUtils.decodeLongitude(encoded[1]),
GeoEncodingUtils.decodeLatitude(encoded[2]),
GeoEncodingUtils.decodeLongitude(encoded[3]),
GeoEncodingUtils.decodeLatitude(encoded[4]),
GeoEncodingUtils.decodeLongitude(encoded[5])};
int orientation = GeoUtils.orient(original[1], original[0], original[3], original[2], original[5], original[4]);
//quantize original
double[] originalQuantize;
//we need to change the orientation if CW
if (orientation == -1) {
originalQuantize = new double[] {GeoEncodingUtils.decodeLatitude(original[4]),
GeoEncodingUtils.decodeLongitude(original[5]),
GeoEncodingUtils.decodeLatitude(original[2]),
GeoEncodingUtils.decodeLongitude(original[3]),
GeoEncodingUtils.decodeLatitude(original[0]),
GeoEncodingUtils.decodeLongitude(original[1])};
} else {
originalQuantize = new double[] {GeoEncodingUtils.decodeLatitude(original[0]),
GeoEncodingUtils.decodeLongitude(original[1]),
GeoEncodingUtils.decodeLatitude(original[2]),
GeoEncodingUtils.decodeLongitude(original[3]),
GeoEncodingUtils.decodeLatitude(original[4]),
GeoEncodingUtils.decodeLongitude(original[5])};
}
for (int i =0; i < 100; i ++) {
Polygon polygon = GeoTestUtil.nextPolygon();
Polygon2D polygon2D = Polygon2D.create(polygon);
PointValues.Relation originalRelation = polygon2D.relateTriangle(originalQuantize[1], originalQuantize[0], originalQuantize[3], originalQuantize[2], originalQuantize[5], originalQuantize[4]);
PointValues.Relation encodedRelation = polygon2D.relateTriangle(encodedQuantize[1], encodedQuantize[0], encodedQuantize[3], encodedQuantize[2], encodedQuantize[5], encodedQuantize[4]);
assertTrue(originalRelation == encodedRelation);
}
}
public void testDegeneratedTriangle() {
double alat = 1e-26d;
double alon = 0.0d;
double blat = -1.0d;
double blon = 0.0d;
double clat = 1.0d;
double clon = 0.0d;
int alatEnc = GeoEncodingUtils.encodeLatitude(alat);
int alonEnc = GeoEncodingUtils.encodeLongitude(alon);
int blatEnc = GeoEncodingUtils.encodeLatitude(blat);
int blonEnc = GeoEncodingUtils.encodeLongitude(blon);
int clatEnc = GeoEncodingUtils.encodeLatitude(clat);
int clonEnc = GeoEncodingUtils.encodeLongitude(clon);
byte[] b = new byte[7 * LatLonShape.BYTES];
LatLonShape.encodeTriangle(b, alatEnc, alonEnc, blatEnc, blonEnc, clatEnc, clonEnc);
int[] encoded = new int[6];
LatLonShape.decodeTriangle(b, encoded);
assertTrue(encoded[0] == blatEnc);
assertTrue(encoded[1] == blonEnc);
assertTrue(encoded[2] == clatEnc);
assertTrue(encoded[3] == clonEnc);
assertTrue(encoded[4] == alatEnc);
assertTrue(encoded[5] == alonEnc);
} }
} }

View File

@ -0,0 +1,124 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.EdgeTree;
import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.ShapeTestUtil;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon2D;
import org.apache.lucene.geo.XYRectangle;
import org.apache.lucene.geo.XYRectangle2D;
import org.apache.lucene.index.PointValues.Relation;
/** random cartesian bounding box, line, and polygon query tests for random generated cartesian {@link XYLine} types */
public class TestXYLineShapeQueries extends BaseXYShapeTestCase {
@Override
protected ShapeType getShapeType() {
return ShapeType.LINE;
}
@Override
protected XYLine randomQueryLine(Object... shapes) {
if (random().nextInt(100) == 42) {
// we want to ensure some cross, so randomly generate lines that share vertices with the indexed point set
int maxBound = (int)Math.floor(shapes.length * 0.1d);
if (maxBound < 2) {
maxBound = shapes.length;
}
float[] x = new float[RandomNumbers.randomIntBetween(random(), 2, maxBound)];
float[] y = new float[x.length];
for (int i = 0, j = 0; j < x.length && i < shapes.length; ++i, ++j) {
XYLine l = (XYLine) (shapes[i]);
if (random().nextBoolean() && l != null) {
int v = random().nextInt(l.numPoints() - 1);
x[j] = (float)l.getX(v);
y[j] = (float)l.getY(v);
} else {
x[j] = (float)ShapeTestUtil.nextDouble();
y[j] = (float)ShapeTestUtil.nextDouble();
}
}
return new XYLine(x, y);
}
return nextLine();
}
@Override
protected Field[] createIndexableFields(String field, Object line) {
return XYShape.createIndexableFields(field, (XYLine)line);
}
@Override
protected Validator getValidator() {
return new LineValidator(this.ENCODER);
}
protected static class LineValidator extends Validator {
protected LineValidator(Encoder encoder) {
super(encoder);
}
@Override
public boolean testBBoxQuery(double minY, double maxY, double minX, double maxX, Object shape) {
XYLine line = (XYLine)shape;
XYRectangle2D rectangle2D = XYRectangle2D.create(new XYRectangle(minX, maxX, minY, maxY));
for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) {
int[] decoded = encoder.encodeDecodeTriangle(line.getX(i), line.getY(i), line.getX(j), line.getY(j), line.getX(i), line.getY(i));
if (queryRelation == QueryRelation.WITHIN) {
if (rectangle2D.containsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == false) {
return false;
}
} else {
if (rectangle2D.intersectsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == true) {
return queryRelation == QueryRelation.INTERSECTS;
}
}
}
return queryRelation != QueryRelation.INTERSECTS;
}
@Override
public boolean testLineQuery(Line2D line2d, Object shape) {
return testLine(line2d, (XYLine) shape);
}
@Override
public boolean testPolygonQuery(Object poly2d, Object shape) {
return testLine((XYPolygon2D)poly2d, (XYLine) shape);
}
private boolean testLine(EdgeTree queryPoly, XYLine line) {
for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) {
double[] qTriangle = encoder.quantizeTriangle(line.getX(i), line.getY(i), line.getX(j), line.getY(j), line.getX(i), line.getY(i));
Relation r = queryPoly.relateTriangle(qTriangle[1], qTriangle[0], qTriangle[3], qTriangle[2], qTriangle[5], qTriangle[4]);
if (queryRelation == QueryRelation.DISJOINT) {
if (r != Relation.CELL_OUTSIDE_QUERY) return false;
} else if (queryRelation == QueryRelation.WITHIN) {
if (r != Relation.CELL_INSIDE_QUERY) return false;
} else {
if (r != Relation.CELL_OUTSIDE_QUERY) return true;
}
}
return queryRelation == QueryRelation.INTERSECTS ? false : true;
}
}
}

View File

@ -0,0 +1,130 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.XYLine;
/** random cartesian bounding box, line, and polygon query tests for random indexed arrays of cartesian {@link XYLine} types */
public class TestXYMultiLineShapeQueries extends BaseXYShapeTestCase {
@Override
protected ShapeType getShapeType() {
return ShapeType.LINE;
}
@Override
protected XYLine[] nextShape() {
int n = random().nextInt(4) + 1;
XYLine[] lines = new XYLine[n];
for (int i =0; i < n; i++) {
lines[i] = nextLine();
}
return lines;
}
@Override
protected Field[] createIndexableFields(String name, Object o) {
XYLine[] lines = (XYLine[]) o;
List<Field> allFields = new ArrayList<>();
for (XYLine line : lines) {
Field[] fields = XYShape.createIndexableFields(name, line);
for (Field field : fields) {
allFields.add(field);
}
}
return allFields.toArray(new Field[allFields.size()]);
}
@Override
public Validator getValidator() {
return new MultiLineValidator(ENCODER);
}
protected class MultiLineValidator extends Validator {
TestXYLineShapeQueries.LineValidator LINEVALIDATOR;
MultiLineValidator(Encoder encoder) {
super(encoder);
LINEVALIDATOR = new TestXYLineShapeQueries.LineValidator(encoder);
}
@Override
public Validator setRelation(QueryRelation relation) {
super.setRelation(relation);
LINEVALIDATOR.queryRelation = relation;
return this;
}
@Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
XYLine[] lines = (XYLine[])shape;
for (XYLine l : lines) {
boolean b = LINEVALIDATOR.testBBoxQuery(minLat, maxLat, minLon, maxLon, l);
if (b == true && queryRelation == ShapeField.QueryRelation.INTERSECTS) {
return true;
} else if (b == false && queryRelation == ShapeField.QueryRelation.DISJOINT) {
return false;
} else if (b == false && queryRelation == ShapeField.QueryRelation.WITHIN) {
return false;
}
}
return queryRelation != ShapeField.QueryRelation.INTERSECTS;
}
@Override
public boolean testLineQuery(Line2D query, Object shape) {
XYLine[] lines = (XYLine[])shape;
for (XYLine l : lines) {
boolean b = LINEVALIDATOR.testLineQuery(query, l);
if (b == true && queryRelation == ShapeField.QueryRelation.INTERSECTS) {
return true;
} else if (b == false && queryRelation == ShapeField.QueryRelation.DISJOINT) {
return false;
} else if (b == false && queryRelation == ShapeField.QueryRelation.WITHIN) {
return false;
}
}
return queryRelation != ShapeField.QueryRelation.INTERSECTS;
}
@Override
public boolean testPolygonQuery(Object query, Object shape) {
XYLine[] lines = (XYLine[])shape;
for (XYLine l : lines) {
boolean b = LINEVALIDATOR.testPolygonQuery(query, l);
if (b == true && queryRelation == ShapeField.QueryRelation.INTERSECTS) {
return true;
} else if (b == false && queryRelation == ShapeField.QueryRelation.DISJOINT) {
return false;
} else if (b == false && queryRelation == ShapeField.QueryRelation.WITHIN) {
return false;
}
}
return queryRelation != ShapeField.QueryRelation.INTERSECTS;
}
}
@Slow
@Nightly
@Override
public void testRandomBig() throws Exception {
doTestRandom(10000);
}
}

View File

@ -0,0 +1,129 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Line2D;
/** random cartesian bounding box, line, and polygon query tests for random indexed arrays of {@code x, y} points */
public class TestXYMultiPointShapeQueries extends BaseXYShapeTestCase {
@Override
protected ShapeType getShapeType() {
return ShapeType.POINT;
}
@Override
protected Point[] nextShape() {
int n = random().nextInt(4) + 1;
Point[] points = new Point[n];
for (int i =0; i < n; i++) {
points[i] = (Point)ShapeType.POINT.nextShape();
}
return points;
}
@Override
protected Field[] createIndexableFields(String name, Object o) {
Point[] points = (Point[]) o;
List<Field> allFields = new ArrayList<>();
for (Point point : points) {
Field[] fields = XYShape.createIndexableFields(name, point.x, point.y);
for (Field field : fields) {
allFields.add(field);
}
}
return allFields.toArray(new Field[allFields.size()]);
}
@Override
public Validator getValidator() {
return new MultiPointValidator(ENCODER);
}
protected class MultiPointValidator extends Validator {
TestXYPointShapeQueries.PointValidator POINTVALIDATOR;
MultiPointValidator(Encoder encoder) {
super(encoder);
POINTVALIDATOR = new TestXYPointShapeQueries.PointValidator(encoder);
}
@Override
public Validator setRelation(QueryRelation relation) {
super.setRelation(relation);
POINTVALIDATOR.queryRelation = relation;
return this;
}
@Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Point[] points = (Point[]) shape;
for (Point p : points) {
boolean b = POINTVALIDATOR.testBBoxQuery(minLat, maxLat, minLon, maxLon, p);
if (b == true && queryRelation == QueryRelation.INTERSECTS) {
return true;
} else if (b == false && queryRelation == QueryRelation.DISJOINT) {
return false;
} else if (b == false && queryRelation == QueryRelation.WITHIN) {
return false;
}
}
return queryRelation != QueryRelation.INTERSECTS;
}
@Override
public boolean testLineQuery(Line2D query, Object shape) {
Point[] points = (Point[]) shape;
for (Point p : points) {
boolean b = POINTVALIDATOR.testLineQuery(query, p);
if (b == true && queryRelation == QueryRelation.INTERSECTS) {
return true;
} else if (b == false && queryRelation == QueryRelation.DISJOINT) {
return false;
} else if (b == false && queryRelation == QueryRelation.WITHIN) {
return false;
}
}
return queryRelation != QueryRelation.INTERSECTS;
}
@Override
public boolean testPolygonQuery(Object query, Object shape) {
Point[] points = (Point[]) shape;
for (Point p : points) {
boolean b = POINTVALIDATOR.testPolygonQuery(query, p);
if (b == true && queryRelation == QueryRelation.INTERSECTS) {
return true;
} else if (b == false && queryRelation == QueryRelation.DISJOINT) {
return false;
} else if (b == false && queryRelation == QueryRelation.WITHIN) {
return false;
}
}
return queryRelation != QueryRelation.INTERSECTS;
}
}
@Slow
@Nightly
@Override
public void testRandomBig() throws Exception {
doTestRandom(10000);
}
}

View File

@ -0,0 +1,142 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.geo.XYPolygon;
/** random cartesian bounding box, line, and polygon query tests for random indexed arrays of cartesian {@link XYPolygon} types */
public class TestXYMultiPolygonShapeQueries extends BaseXYShapeTestCase {
@Override
protected ShapeType getShapeType() {
return ShapeType.POLYGON;
}
@Override
protected XYPolygon[] nextShape() {
int n = random().nextInt(4) + 1;
XYPolygon[] polygons = new XYPolygon[n];
for (int i =0; i < n; i++) {
while (true) {
// if we can't tessellate; then random polygon generator created a malformed shape
XYPolygon p = (XYPolygon) getShapeType().nextShape();
try {
Tessellator.tessellate(p);
polygons[i] = p;
break;
} catch (IllegalArgumentException e) {
continue;
}
}
}
return polygons;
}
@Override
protected Field[] createIndexableFields(String name, Object o) {
XYPolygon[] polygons = (XYPolygon[]) o;
List<Field> allFields = new ArrayList<>();
for (XYPolygon polygon : polygons) {
Field[] fields = XYShape.createIndexableFields(name, polygon);
for (Field field : fields) {
allFields.add(field);
}
}
return allFields.toArray(new Field[allFields.size()]);
}
@Override
protected Validator getValidator() {
return new MultiPolygonValidator(ENCODER);
}
protected class MultiPolygonValidator extends Validator {
TestXYPolygonShapeQueries.PolygonValidator POLYGONVALIDATOR;
MultiPolygonValidator(Encoder encoder) {
super(encoder);
POLYGONVALIDATOR = new TestXYPolygonShapeQueries.PolygonValidator(encoder);
}
@Override
public Validator setRelation(QueryRelation relation) {
super.setRelation(relation);
POLYGONVALIDATOR.queryRelation = relation;
return this;
}
@Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
XYPolygon[] polygons = (XYPolygon[])shape;
for (XYPolygon p : polygons) {
boolean b = POLYGONVALIDATOR.testBBoxQuery(minLat, maxLat, minLon, maxLon, p);
if (b == true && queryRelation == QueryRelation.INTERSECTS) {
return true;
} else if (b == false && queryRelation == QueryRelation.DISJOINT) {
return false;
} else if (b == false && queryRelation == QueryRelation.WITHIN) {
return false;
}
}
return queryRelation != QueryRelation.INTERSECTS;
}
@Override
public boolean testLineQuery(Line2D query, Object shape) {
XYPolygon[] polygons = (XYPolygon[])shape;
for (XYPolygon p : polygons) {
boolean b = POLYGONVALIDATOR.testLineQuery(query, p);
if (b == true && queryRelation == QueryRelation.INTERSECTS) {
return true;
} else if (b == false && queryRelation == QueryRelation.DISJOINT) {
return false;
} else if (b == false && queryRelation == QueryRelation.WITHIN) {
return false;
}
}
return queryRelation != QueryRelation.INTERSECTS;
}
@Override
public boolean testPolygonQuery(Object query, Object shape) {
XYPolygon[] polygons = (XYPolygon[])shape;
for (XYPolygon p : polygons) {
boolean b = POLYGONVALIDATOR.testPolygonQuery(query, p);
if (b == true && queryRelation == QueryRelation.INTERSECTS) {
return true;
} else if (b == false && queryRelation == QueryRelation.DISJOINT) {
return false;
} else if (b == false && queryRelation == QueryRelation.WITHIN) {
return false;
}
}
return queryRelation != QueryRelation.INTERSECTS;
}
}
@Slow
@Nightly
@Override
public void testRandomBig() throws Exception {
doTestRandom(10000);
}
}

View File

@ -0,0 +1,116 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.EdgeTree;
import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.ShapeTestUtil;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon2D;
import org.apache.lucene.index.PointValues.Relation;
/** random cartesian bounding box, line, and polygon query tests for random generated {@code x, y} points */
public class TestXYPointShapeQueries extends BaseXYShapeTestCase {
@Override
protected ShapeType getShapeType() {
return ShapeType.POINT;
}
@Override
protected XYLine randomQueryLine(Object... shapes) {
if (random().nextInt(100) == 42) {
// we want to ensure some cross, so randomly generate lines that share vertices with the indexed point set
int maxBound = (int)Math.floor(shapes.length * 0.1d);
if (maxBound < 2) {
maxBound = shapes.length;
}
float[] x = new float[RandomNumbers.randomIntBetween(random(), 2, maxBound)];
float[] y = new float[x.length];
for (int i = 0, j = 0; j < x.length && i < shapes.length; ++i, ++j) {
Point p = (Point) (shapes[i]);
if (random().nextBoolean() && p != null) {
x[j] = p.x;
y[j] = p.y;
} else {
x[j] = (float)ShapeTestUtil.nextDouble();
y[j] = (float)ShapeTestUtil.nextDouble();
}
}
return new XYLine(x, y);
}
return nextLine();
}
@Override
protected Field[] createIndexableFields(String field, Object point) {
Point p = (Point)point;
return XYShape.createIndexableFields(field, p.x, p.y);
}
@Override
protected Validator getValidator() {
return new PointValidator(this.ENCODER);
}
protected static class PointValidator extends Validator {
protected PointValidator(Encoder encoder) {
super(encoder);
}
@Override
public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
Point p = (Point)shape;
double lat = encoder.quantizeY(p.y);
double lon = encoder.quantizeX(p.x);
boolean isDisjoint = lat < minLat || lat > maxLat;
isDisjoint = isDisjoint || ((minLon > maxLon)
? lon < minLon && lon > maxLon
: lon < minLon || lon > maxLon);
if (queryRelation == QueryRelation.DISJOINT) {
return isDisjoint;
}
return isDisjoint == false;
}
@Override
public boolean testLineQuery(Line2D line2d, Object shape) {
return testPoint(line2d, (Point) shape);
}
@Override
public boolean testPolygonQuery(Object poly2d, Object shape) {
return testPoint((XYPolygon2D)poly2d, (Point) shape);
}
private boolean testPoint(EdgeTree tree, Point p) {
double lat = encoder.quantizeY(p.y);
double lon = encoder.quantizeX(p.x);
// for consistency w/ the query we test the point as a triangle
Relation r = tree.relateTriangle(lon, lat, lon, lat, lon, lat);
if (queryRelation == QueryRelation.WITHIN) {
return r == Relation.CELL_INSIDE_QUERY;
} else if (queryRelation == QueryRelation.DISJOINT) {
return r == Relation.CELL_OUTSIDE_QUERY;
}
return r != Relation.CELL_OUTSIDE_QUERY;
}
}
}

View File

@ -0,0 +1,122 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import java.util.List;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.EdgeTree;
import org.apache.lucene.geo.Line2D;
import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.geo.XYPolygon2D;
import org.apache.lucene.geo.XYRectangle;
import org.apache.lucene.geo.XYRectangle2D;
import org.apache.lucene.index.PointValues.Relation;
/** random cartesian bounding box, line, and polygon query tests for random indexed {@link XYPolygon} types */
public class TestXYPolygonShapeQueries extends BaseXYShapeTestCase {
@Override
protected ShapeType getShapeType() {
return ShapeType.POLYGON;
}
@Override
protected XYPolygon nextShape() {
XYPolygon p;
while (true) {
// if we can't tessellate; then random polygon generator created a malformed shape
p = (XYPolygon)getShapeType().nextShape();
try {
Tessellator.tessellate(p);
return p;
} catch (IllegalArgumentException e) {
continue;
}
}
}
@Override
protected Field[] createIndexableFields(String field, Object polygon) {
return XYShape.createIndexableFields(field, (XYPolygon)polygon);
}
@Override
protected Validator getValidator() {
return new PolygonValidator(this.ENCODER);
}
protected static class PolygonValidator extends Validator {
protected PolygonValidator(Encoder encoder) {
super(encoder);
}
@Override
public boolean testBBoxQuery(double minY, double maxY, double minX, double maxX, Object shape) {
XYPolygon p = (XYPolygon)shape;
XYRectangle2D rectangle2D = XYRectangle2D.create(new XYRectangle(minX, maxX, minY, maxY));
List<Tessellator.Triangle> tessellation = Tessellator.tessellate(p);
for (Tessellator.Triangle t : tessellation) {
int[] decoded = encoder.encodeDecodeTriangle(t.getX(0), t.getY(0), t.getX(1), t.getY(1), t.getX(2), t.getY(2));
if (queryRelation == QueryRelation.WITHIN) {
if (rectangle2D.containsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == false) {
return false;
}
} else {
if (rectangle2D.intersectsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == true) {
return queryRelation == QueryRelation.INTERSECTS;
}
}
}
return queryRelation != QueryRelation.INTERSECTS;
}
@Override
public boolean testLineQuery(Line2D query, Object shape) {
return testPolygon(query, (XYPolygon) shape);
}
@Override
public boolean testPolygonQuery(Object query, Object shape) {
return testPolygon((XYPolygon2D)query, (XYPolygon) shape);
}
private boolean testPolygon(EdgeTree tree, XYPolygon shape) {
List<Tessellator.Triangle> tessellation = Tessellator.tessellate(shape);
for (Tessellator.Triangle t : tessellation) {
double[] qTriangle = encoder.quantizeTriangle(t.getX(0), t.getY(0), t.getX(1), t.getY(1), t.getX(2), t.getY(2));
Relation r = tree.relateTriangle(qTriangle[1], qTriangle[0], qTriangle[3], qTriangle[2], qTriangle[5], qTriangle[4]);
if (queryRelation == QueryRelation.DISJOINT) {
if (r != Relation.CELL_OUTSIDE_QUERY) return false;
} else if (queryRelation == QueryRelation.WITHIN) {
if (r != Relation.CELL_INSIDE_QUERY) return false;
} else {
if (r != Relation.CELL_OUTSIDE_QUERY) return true;
}
}
return queryRelation == QueryRelation.INTERSECTS ? false : true;
}
}
@Nightly
@Override
public void testRandomBig() throws Exception {
doTestRandom(25000);
}
}

View File

@ -0,0 +1,111 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.ShapeTestUtil;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.TestUtil;
/** Test case for indexing cartesian shapes and search by bounding box, lines, and polygons */
public class TestXYShape extends LuceneTestCase {
protected static String FIELDNAME = "field";
protected static void addPolygonsToDoc(String field, Document doc, XYPolygon polygon) {
Field[] fields = XYShape.createIndexableFields(field, polygon);
for (Field f : fields) {
doc.add(f);
}
}
protected static void addLineToDoc(String field, Document doc, XYLine line) {
Field[] fields = XYShape.createIndexableFields(field, line);
for (Field f : fields) {
doc.add(f);
}
}
protected Query newRectQuery(String field, double minX, double maxX, double minY, double maxY) {
return XYShape.newBoxQuery(field, QueryRelation.INTERSECTS, (float)minX, (float)maxX, (float)minY, (float)maxY);
}
/** test we can search for a point with a standard number of vertices*/
public void testBasicIntersects() throws Exception {
int numVertices = TestUtil.nextInt(random(), 50, 100);
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
// add a random polygon document
XYPolygon p = ShapeTestUtil.createRegularPolygon(0, 90, atLeast(1000000), numVertices);
Document document = new Document();
addPolygonsToDoc(FIELDNAME, document, p);
writer.addDocument(document);
// add a line document
document = new Document();
// add a line string
float x[] = new float[p.numPoints() - 1];
float y[] = new float[p.numPoints() - 1];
for (int i = 0; i < x.length; ++i) {
x[i] = (float)p.getPolyX(i);
y[i] = (float)p.getPolyY(i);
}
XYLine l = new XYLine(x, y);
addLineToDoc(FIELDNAME, document, l);
writer.addDocument(document);
////// search /////
// search an intersecting bbox
IndexReader reader = writer.getReader();
writer.close();
IndexSearcher searcher = newSearcher(reader);
double minX = Math.min(x[0], x[1]);
double minY = Math.min(y[0], y[1]);
double maxX = Math.max(x[0], x[1]);
double maxY = Math.max(y[0], y[1]);
Query q = newRectQuery(FIELDNAME, minX, maxX, minY, maxY);
assertEquals(2, searcher.count(q));
// search a disjoint bbox
q = newRectQuery(FIELDNAME, p.minX-1d, p.minX+1, p.minY-1d, p.minY+1d);
assertEquals(0, searcher.count(q));
// search w/ an intersecting polygon
q = XYShape.newPolygonQuery(FIELDNAME, QueryRelation.INTERSECTS, new XYPolygon(
new float[] {(float)minX, (float)minX, (float)maxX, (float)maxX, (float)minX},
new float[] {(float)minY, (float)maxY, (float)maxY, (float)minY, (float)minY}
));
assertEquals(2, searcher.count(q));
// search w/ an intersecting line
q = XYShape.newLineQuery(FIELDNAME, QueryRelation.INTERSECTS, new XYLine(
new float[] {(float)minX, (float)minX, (float)maxX, (float)maxX},
new float[] {(float)minY, (float)maxY, (float)maxY, (float)minY}
));
assertEquals(2, searcher.count(q));
IOUtils.close(reader, dir);
}
}

View File

@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.document;
import org.apache.lucene.geo.ShapeTestUtil;
import org.apache.lucene.geo.XYEncodingUtils;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.geo.XYPolygon2D;
/** tests XYShape encoding */
public class TestXYShapeEncoding extends BaseShapeEncodingTestCase {
@Override
protected int encodeX(double x) {
return XYEncodingUtils.encode(x);
}
@Override
protected int encodeY(double y) {
return XYEncodingUtils.encode(y);
}
@Override
protected double decodeX(int xEncoded) {
return XYEncodingUtils.decode(xEncoded);
}
@Override
protected double decodeY(int yEncoded) {
return XYEncodingUtils.decode(yEncoded);
}
@Override
protected double nextX() {
return ShapeTestUtil.nextDouble();
}
@Override
protected double nextY() {
return ShapeTestUtil.nextDouble();
}
@Override
protected XYPolygon nextPolygon() {
return ShapeTestUtil.nextPolygon();
}
@Override
protected XYPolygon2D createPolygon2D(Object polygon) {
return XYPolygon2D.create((XYPolygon)polygon);
}
}

View File

@ -0,0 +1,208 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.lucene.geo;
import java.util.ArrayList;
import java.util.Random;
import com.carrotsearch.randomizedtesting.RandomizedContext;
import com.carrotsearch.randomizedtesting.generators.BiasedNumbers;
import org.apache.lucene.util.SloppyMath;
import org.apache.lucene.util.TestUtil;
/** generates random cartesian geometry; heavy reuse of {@link GeoTestUtil} */
public class ShapeTestUtil {
/** returns next pseudorandom polygon */
public static XYPolygon nextPolygon() {
if (random().nextBoolean()) {
return surpriseMePolygon();
} else if (random().nextInt(10) == 1) {
// this poly is slow to create ... only do it 10% of the time:
while (true) {
int gons = TestUtil.nextInt(random(), 4, 500);
// So the poly can cover at most 50% of the earth's surface:
double radius = random().nextDouble() * 0.5 * Float.MAX_VALUE + 1.0;
try {
return createRegularPolygon(nextDouble(), nextDouble(), radius, gons);
} catch (IllegalArgumentException iae) {
// we tried to cross dateline or pole ... try again
}
}
}
XYRectangle box = nextBoxInternal();
if (random().nextBoolean()) {
// box
return boxPolygon(box);
} else {
// triangle
return trianglePolygon(box);
}
}
private static XYPolygon trianglePolygon(XYRectangle box) {
final float[] polyX = new float[4];
final float[] polyY = new float[4];
polyX[0] = (float)box.minX;
polyY[0] = (float)box.minY;
polyX[1] = (float)box.minX;
polyY[1] = (float)box.minY;
polyX[2] = (float)box.minX;
polyY[2] = (float)box.minY;
polyX[3] = (float)box.minX;
polyY[3] = (float)box.minY;
return new XYPolygon(polyX, polyY);
}
public static XYRectangle nextBox() {
return nextBoxInternal();
}
private static XYRectangle nextBoxInternal() {
// prevent lines instead of boxes
double x0 = nextDouble();
double x1 = nextDouble();
while (x0 == x1) {
x1 = nextDouble();
}
// prevent lines instead of boxes
double y0 = nextDouble();
double y1 = nextDouble();
while (y0 == y1) {
y1 = nextDouble();
}
if (x1 < x0) {
double x = x0;
x0 = x1;
x1 = x;
}
if (y1 < y0) {
double y = y0;
y0 = y1;
y1 = y;
}
return new XYRectangle(x0, x1, y0, y1);
}
private static XYPolygon boxPolygon(XYRectangle box) {
final float[] polyX = new float[5];
final float[] polyY = new float[5];
polyX[0] = (float)box.minX;
polyY[0] = (float)box.minY;
polyX[1] = (float)box.minX;
polyY[1] = (float)box.minY;
polyX[2] = (float)box.minX;
polyY[2] = (float)box.minY;
polyX[3] = (float)box.minX;
polyY[3] = (float)box.minY;
polyX[4] = (float)box.minX;
polyY[4] = (float)box.minY;
return new XYPolygon(polyX, polyY);
}
private static XYPolygon surpriseMePolygon() {
// repeat until we get a poly that doesn't cross dateline:
while (true) {
//System.out.println("\nPOLY ITER");
double centerX = nextDouble();
double centerY = nextDouble();
double radius = 0.1 + 20 * random().nextDouble();
double radiusDelta = random().nextDouble();
ArrayList<Float> xList = new ArrayList<>();
ArrayList<Float> yList = new ArrayList<>();
double angle = 0.0;
while (true) {
angle += random().nextDouble()*40.0;
//System.out.println(" angle " + angle);
if (angle > 360) {
break;
}
double len = radius * (1.0 - radiusDelta + radiusDelta * random().nextDouble());
double maxX = StrictMath.min(StrictMath.abs(Float.MAX_VALUE - centerX), StrictMath.abs(-Float.MAX_VALUE - centerX));
double maxY = StrictMath.min(StrictMath.abs(Float.MAX_VALUE - centerY), StrictMath.abs(-Float.MAX_VALUE - centerY));
len = StrictMath.min(len, StrictMath.min(maxX, maxY));
//System.out.println(" len=" + len);
float x = (float)(centerX + len * Math.cos(SloppyMath.toRadians(angle)));
float y = (float)(centerY + len * Math.sin(SloppyMath.toRadians(angle)));
xList.add(x);
yList.add(y);
//System.out.println(" lat=" + lats.get(lats.size()-1) + " lon=" + lons.get(lons.size()-1));
}
// close it
xList.add(xList.get(0));
yList.add(yList.get(0));
float[] xArray = new float[xList.size()];
float[] yArray = new float[yList.size()];
for(int i=0;i<xList.size();i++) {
xArray[i] = xList.get(i);
yArray[i] = yList.get(i);
}
return new XYPolygon(xArray, yArray);
}
}
/** Makes an n-gon, centered at the provided x/y, and each vertex approximately
* distanceMeters away from the center.
*
* Do not invoke me across the dateline or a pole!! */
public static XYPolygon createRegularPolygon(double centerX, double centerY, double radius, int gons) {
double maxX = StrictMath.min(StrictMath.abs(Float.MAX_VALUE - centerX), StrictMath.abs(-Float.MAX_VALUE - centerX));
double maxY = StrictMath.min(StrictMath.abs(Float.MAX_VALUE - centerY), StrictMath.abs(-Float.MAX_VALUE - centerY));
radius = StrictMath.min(radius, StrictMath.min(maxX, maxY));
float[][] result = new float[2][];
result[0] = new float[gons+1];
result[1] = new float[gons+1];
//System.out.println("make gon=" + gons);
for(int i=0;i<gons;i++) {
double angle = 360.0-i*(360.0/gons);
//System.out.println(" angle " + angle);
double x = Math.cos(StrictMath.toRadians(angle));
double y = Math.sin(StrictMath.toRadians(angle));
result[0][i] = (float)(centerY + y * radius);
result[1][i] = (float)(centerX + x * radius);
}
// close poly
result[0][gons] = result[0][0];
result[1][gons] = result[1][0];
return new XYPolygon(result[0], result[1]);
}
public static double nextDouble() {
return BiasedNumbers.randomDoubleBetween(random(), -Float.MAX_VALUE, Float.MAX_VALUE);
}
/** Keep it simple, we don't need to take arbitrary Random for geo tests */
private static Random random() {
return RandomizedContext.current().getRandom();
}
}

View File

@ -60,7 +60,7 @@ public class TestLine2D extends LuceneTestCase {
} }
public void testRandomTriangles() { public void testRandomTriangles() {
Line line = TestLatLonLineShapeQueries.nextLine(); Line line = TestLatLonLineShapeQueries.getNextLine();
Line2D line2D = Line2D.create(line); Line2D line2D = Line2D.create(line);
for (int i =0; i < 100; i++) { for (int i =0; i < 100; i++) {

View File

@ -572,8 +572,8 @@ public class TestTessellator extends LuceneTestCase {
private double area(List<Tessellator.Triangle> triangles) { private double area(List<Tessellator.Triangle> triangles) {
double area = 0; double area = 0;
for (Tessellator.Triangle t : triangles) { for (Tessellator.Triangle t : triangles) {
double[] lats = new double[] {t.getLat(0), t.getLat(1), t.getLat(2), t.getLat(0)}; double[] lats = new double[] {t.getY(0), t.getY(1), t.getY(2), t.getY(0)};
double[] lons = new double[] {t.getLon(0), t.getLon(1), t.getLon(2), t.getLon(0)}; double[] lons = new double[] {t.getX(0), t.getX(1), t.getX(2), t.getX(0)};
area += area(new Polygon(lats, lons)); area += area(new Polygon(lats, lons));
} }
return area; return area;