diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index a56bc8327878..73f23e72ea88 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -65,6 +65,9 @@ API Changes 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. (Gert Morten Paimla via Tomoko Uchida) diff --git a/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java b/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java index 277d02b7954b..49dfefa5ac2e 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java @@ -29,18 +29,22 @@ */ // 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. -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, // and pull up max values for both dimensions to each parent node (regardless of split). /** tree of holes, or null */ - private final Polygon2D holes; + protected final Polygon2D holes; private final AtomicBoolean containsBoundary = new AtomicBoolean(false); - private Polygon2D(Polygon polygon, Polygon2D holes) { - super(polygon.minLat, polygon.maxLat, polygon.minLon, polygon.maxLon, polygon.getPolyLats(), polygon.getPolyLons()); + protected Polygon2D(final double minLat, final double maxLat, final double minLon, final double maxLon, double[] lats, double[] lons, Polygon2D holes) { + super(minLat, maxLat, minLon, maxLon, lats, lons); 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. *

diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java index ee41ffa51fce..cd5405904317 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java @@ -19,31 +19,29 @@ import java.util.ArrayList; 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.Polygon; import org.apache.lucene.geo.Tessellator; -import org.apache.lucene.geo.Tessellator.Triangle; -import org.apache.lucene.index.PointValues; +import org.apache.lucene.index.PointValues; // javadoc 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.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). *

- * {@link Polygon}'s are decomposed into a triangular mesh using the {@link Tessellator} utility class - * Each {@link Triangle} is encoded and indexed as a multi-value field. - *

- * Finding all shapes that intersect a range (e.g., bounding box) at search time is efficient. - *

- * This class defines static factory methods for common operations: + * This class defines six static factory methods for common indexing and search operations: *

* WARNING: Like {@link LatLonPoint}, vertex values are indexed with some loss of precision from the @@ -55,13 +53,6 @@ * @lucene.experimental */ 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: private LatLonShape() { @@ -70,10 +61,10 @@ private LatLonShape() { /** create indexable fields for polygon geometry */ public static Field[] createIndexableFields(String fieldName, Polygon polygon) { // the lionshare of the indexing is done by the tessellator - List tessellation = Tessellator.tessellate(polygon); - List fields = new ArrayList<>(); - for (Triangle t : tessellation) { - fields.add(new LatLonTriangle(fieldName, t)); + List tessellation = Tessellator.tessellate(polygon); + List fields = new ArrayList<>(); + for (Tessellator.Triangle t : tessellation) { + fields.add(new Triangle(fieldName, t)); } return fields.toArray(new Field[fields.size()]); } @@ -84,286 +75,38 @@ public static Field[] createIndexableFields(String fieldName, Line line) { Field[] fields = new Field[numPoints - 1]; // create "flat" triangles 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; } /** create indexable fields for point geometry */ 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) { 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 **/ public static Query newLineQuery(String field, QueryRelation queryRelation, Line... 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 **/ public static Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... 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; - } } diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java index 44cd747aba2b..5645645da26a 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java @@ -16,23 +16,24 @@ */ package org.apache.lucene.document; +import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Rectangle; import org.apache.lucene.geo.Rectangle2D; 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. * *

The field must be indexed using * {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document. * * @lucene.experimental **/ -final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery { +final class LatLonShapeBoundingBoxQuery extends ShapeQuery { final Rectangle rectangle; 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); this.rectangle = new Rectangle(minLat, maxLat, minLon, maxLon); this.rectangle2D = Rectangle2D.create(this.rectangle); @@ -46,9 +47,9 @@ protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] /** returns true if the query matches the encoded triangle */ @Override - protected boolean queryMatches(byte[] t, int[] scratchTriangle, LatLonShape.QueryRelation queryRelation) { + protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) { // decode indexed triangle - LatLonShape.decodeTriangle(t, scratchTriangle); + ShapeField.decodeTriangle(t, scratchTriangle); int aY = scratchTriangle[0]; int aX = scratchTriangle[1]; @@ -57,7 +58,7 @@ protected boolean queryMatches(byte[] t, int[] scratchTriangle, LatLonShape.Quer int cY = scratchTriangle[4]; int cX = scratchTriangle[5]; - if (queryRelation == LatLonShape.QueryRelation.WITHIN) { + if (queryRelation == QueryRelation.WITHIN) { return rectangle2D.containsTriangle(aX, aY, bX, bY, cX, cY); } return rectangle2D.intersectsTriangle(aX, aY, bX, bY, cX, cY); diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeLineQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeLineQuery.java index fcc2590dfdf4..93705650e3d2 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeLineQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeLineQuery.java @@ -18,7 +18,7 @@ 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.Line; import org.apache.lucene.geo.Line2D; @@ -26,7 +26,7 @@ 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}. *

* Note: *

    @@ -43,7 +43,7 @@ * * @lucene.experimental **/ -final class LatLonShapeLineQuery extends LatLonShapeQuery { +final class LatLonShapeLineQuery extends ShapeQuery { final Line[] lines; final private Line2D line2D; @@ -85,7 +85,7 @@ protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] @Override protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) { - LatLonShape.decodeTriangle(t, scratchTriangle); + ShapeField.decodeTriangle(t, scratchTriangle); double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle[0]); double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle[1]); @@ -94,7 +94,7 @@ protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation qu double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle[4]); 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; } // INTERSECTS diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePolygonQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePolygonQuery.java index 97e739ea5b76..bcdd3ae5e454 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePolygonQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePolygonQuery.java @@ -18,7 +18,7 @@ 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.Polygon; import org.apache.lucene.geo.Polygon2D; @@ -26,14 +26,19 @@ 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. + *

    + * Note: + *

      + *
    • Dateline crossing is not yet supported. Polygons should be cut at the dateline and provided as a multipolygon query
    • + *
    * *

    The field must be indexed using * {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document. * * @lucene.experimental **/ -final class LatLonShapePolygonQuery extends LatLonShapeQuery { +final class LatLonShapePolygonQuery extends ShapeQuery { final Polygon[] polygons; final private Polygon2D poly2D; @@ -74,7 +79,7 @@ protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] @Override protected boolean queryMatches(byte[] t, int[] scratchTriangle, QueryRelation queryRelation) { - LatLonShape.decodeTriangle(t, scratchTriangle); + ShapeField.decodeTriangle(t, scratchTriangle); double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle[0]); double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle[1]); diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/ShapeField.java b/lucene/sandbox/src/java/org/apache/lucene/document/ShapeField.java new file mode 100644 index 000000000000..e4e9eaa04fe5 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/ShapeField.java @@ -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. + *

    + * {@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. + *

    + * Finding all shapes that intersect a range (e.g., bounding box), or target shape, at search time is efficient. + *

    + * 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. + *

    + * + * @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; + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/ShapeQuery.java similarity index 83% rename from lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java rename to lucene/sandbox/src/java/org/apache/lucene/document/ShapeQuery.java index 691edcf8377a..a290b685fc61 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/ShapeQuery.java @@ -19,7 +19,7 @@ import java.io.IOException; 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.LeafReader; import org.apache.lucene.index.LeafReaderContext; @@ -41,23 +41,32 @@ import org.apache.lucene.util.FixedBitSet; /** - * Base LatLonShape Query class providing common query logic for - * {@link LatLonShapeBoundingBoxQuery} and {@link LatLonShapePolygonQuery} + * Base query class for all spatial geometries: {@link LatLonShape} and {@link XYShape}. * - * Note: this class implements the majority of the INTERSECTS, WITHIN, DISJOINT relation logic + *

    The field must be indexed using either {@link LatLonShape#createIndexableFields} or + * {@link XYShape#createIndexableFields} and the corresponding factory method must be used: + *

      + *
    • {@link LatLonShape#newBoxQuery newBoxQuery()} for matching geo shapes that have some {@link QueryRelation} with a bounding box. + *
    • {@link LatLonShape#newLineQuery newLineQuery()} for matching geo shapes that have some {@link QueryRelation} with a linestring. + *
    • {@link LatLonShape#newPolygonQuery newPolygonQuery()} for matching geo shapes that have some {@link QueryRelation} with a polygon. + *
    • {@link XYShape#newBoxQuery newBoxQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a bounding box. + *
    • {@link XYShape#newLineQuery newLineQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a linestring. + *
    • {@link XYShape#newPolygonQuery newPolygonQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a polygon. + *
    + *

    * - * @lucene.experimental + * @lucene.experimental **/ -abstract class LatLonShapeQuery extends Query { +abstract class ShapeQuery extends Query { /** field name */ final String field; /** query relation * disjoint: {@code CELL_OUTSIDE_QUERY} * intersects: {@code CELL_CROSSES_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) { throw new IllegalArgumentException("field must not be null"); } @@ -74,12 +83,12 @@ protected abstract Relation relateRangeBBoxToQuery(int minXOffset, int minYOffse int maxXOffset, int maxYOffset, byte[] maxTriangle); /** 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 */ protected Relation relateRangeToQuery(byte[] minTriangle, byte[] maxTriangle, QueryRelation queryRelation) { // 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) { return transposeRelation(r); } @@ -133,18 +142,18 @@ public void visit(DocIdSetIterator iterator, byte[] t) throws IOException { @Override 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) */ - protected IntersectVisitor getDenseIntersectVisitor(FixedBitSet intersect, FixedBitSet disjoint, QueryRelation queryRelation) { + protected IntersectVisitor getDenseIntersectVisitor(FixedBitSet intersect, FixedBitSet disjoint, ShapeField.QueryRelation queryRelation) { return new IntersectVisitor() { final int[] scratchTriangle = new int[6]; @Override 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 disjoint.set(docID); } else { @@ -189,25 +198,25 @@ protected ScorerSupplier getIntersectScorerSupplier(LeafReader reader, PointValu return new RelationScorerSupplier(values, visitor, null, queryRelation) { @Override 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) */ 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); } //For within and disjoint we need two passes to remove false positives in case of multi-shapes. FixedBitSet within = new FixedBitSet(reader.maxDoc()); FixedBitSet disjoint = new FixedBitSet(reader.maxDoc()); - IntersectVisitor withinVisitor = getDenseIntersectVisitor(within, disjoint, QueryRelation.WITHIN); - IntersectVisitor disjointVisitor = getDenseIntersectVisitor(within, disjoint, QueryRelation.DISJOINT); + IntersectVisitor withinVisitor = getDenseIntersectVisitor(within, disjoint, ShapeField.QueryRelation.WITHIN); + IntersectVisitor disjointVisitor = getDenseIntersectVisitor(within, disjoint, ShapeField.QueryRelation.DISJOINT); return new RelationScorerSupplier(values, withinVisitor, disjointVisitor, queryRelation) { @Override 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 @@ public boolean equals(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 */ @@ -308,7 +317,7 @@ private static abstract class RelationScorerSupplier extends ScorerSupplier { PointValues values; IntersectVisitor visitor; IntersectVisitor disjointVisitor;//it can be null - QueryRelation queryRelation; + ShapeField.QueryRelation queryRelation; long cost = -1; RelationScorerSupplier(PointValues values, IntersectVisitor visitor, IntersectVisitor disjointVisitor, QueryRelation queryRelation) { @@ -319,7 +328,7 @@ private static abstract class RelationScorerSupplier extends ScorerSupplier { } /** 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() { int[] scratchTriangle = new int[6]; @Override @@ -354,7 +363,7 @@ public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { } /** 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 { if (values.getDocCount() == reader.maxDoc() && values.getDocCount() == values.size() @@ -376,17 +385,17 @@ && cost() > reader.maxDoc() / 2) { } /** 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 { values.intersect(visitor); if (disjointVisitor != null) { values.intersect(disjointVisitor); } DocIdSetIterator iterator; - if (query.queryRelation == QueryRelation.DISJOINT) { + if (query.queryRelation == ShapeField.QueryRelation.DISJOINT) { disjoint.andNot(intersect); iterator = new BitSetIterator(disjoint, cost()); - } else if (query.queryRelation == QueryRelation.WITHIN) { + } else if (query.queryRelation == ShapeField.QueryRelation.WITHIN) { intersect.andNot(disjoint); iterator = new BitSetIterator(intersect, cost()); } else { @@ -399,7 +408,7 @@ protected Scorer getScorer(LatLonShapeQuery query, Weight weight, public long cost() { if (cost == -1) { // 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); } else { cost = values.estimatePointCount(visitor); diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java b/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java new file mode 100644 index 000000000000..d55356fb8077 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java @@ -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. + *

    + * This class defines six static factory methods for common indexing and search operations: + *

      + *
    • {@link #createIndexableFields(String, XYPolygon)} for indexing a cartesian polygon. + *
    • {@link #createIndexableFields(String, XYLine)} for indexing a cartesian linestring. + *
    • {@link #createIndexableFields(String, float, float)} for indexing a x, y cartesian point. + *
    • {@link #newBoxQuery newBoxQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a bounding box. + *
    • {@link #newBoxQuery newLineQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a linestring. + *
    • {@link #newBoxQuery newPolygonQuery()} for matching cartesian shapes that have some {@link QueryRelation} with a polygon. + *
    + + * WARNING: 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 tessellation = Tessellator.tessellate(polygon); + List 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); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/XYShapeBoundingBoxQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/XYShapeBoundingBoxQuery.java new file mode 100644 index 000000000000..21fa5b48d51d --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/XYShapeBoundingBoxQuery.java @@ -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. + * + *

    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(); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/XYShapeLineQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/XYShapeLineQuery.java new file mode 100644 index 000000000000..b8ec71094d7f --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/XYShapeLineQuery.java @@ -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}. + *

    + * Note: + *

      + *
    • {@code QueryRelation.WITHIN} queries are not yet supported
    • + *
    + *

    + * todo: + *

      + *
    • Add distance support for buffered queries
    • + *
    + *

    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; + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/XYShapePolygonQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/XYShapePolygonQuery.java new file mode 100644 index 000000000000..e1b4e9916b37 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/XYShapePolygonQuery.java @@ -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}. + * + *

    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; + } +} \ No newline at end of file diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java b/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java index 489e5cf0a3d1..3c28607218cb 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java @@ -129,9 +129,9 @@ public String toString() { sb.append("LINE("); for (int i = 0; i < lats.length; i++) { sb.append("[") - .append(lats[i]) - .append(", ") .append(lons[i]) + .append(", ") + .append(lats[i]) .append("]"); } sb.append(')'); diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/Line2D.java b/lucene/sandbox/src/java/org/apache/lucene/geo/Line2D.java index 9f413195b8f3..3c7f4d110515 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/geo/Line2D.java +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/Line2D.java @@ -21,7 +21,7 @@ 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. *

    * 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. @@ -33,6 +33,10 @@ private Line2D(Line line) { 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 */ public static Line2D create(Line... lines) { Line2D components[] = new Line2D[lines.length]; @@ -42,6 +46,15 @@ public static Line2D create(Line... lines) { 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 protected Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) { if (tree.crossesBox(minLat, maxLat, minLon, maxLon, true)) { diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/Rectangle2D.java b/lucene/sandbox/src/java/org/apache/lucene/geo/Rectangle2D.java index 223d000abfcc..c2005373799a 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/geo/Rectangle2D.java +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/Rectangle2D.java @@ -18,8 +18,9 @@ package org.apache.lucene.geo; 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 static java.lang.Integer.BYTES; @@ -32,19 +33,19 @@ import static org.apache.lucene.geo.GeoUtils.orient; /** - * 2D rectangle implementation containing spatial logic. + * 2D rectangle implementation containing geo spatial logic. * * @lucene.internal */ public class Rectangle2D { - final byte[] bbox; - final byte[] west; - final int minX; - final int maxX; - final int minY; - final int maxY; - - private Rectangle2D(double minLat, double maxLat, double minLon, double maxLon) { + protected final byte[] bbox; + private final byte[] west; + protected final int minX; + protected final int maxX; + protected final int minY; + protected final int maxY; + + protected Rectangle2D(double minLat, double maxLat, double minLon, double maxLon) { this.bbox = new byte[4 * BYTES]; int minXenc = encodeLongitudeCeil(minLon); int maxXenc = encodeLongitude(maxLon); @@ -76,6 +77,16 @@ private Rectangle2D(double minLat, double maxLat, double minLon, double maxLon) } } + 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 */ public static Rectangle2D create(Rectangle rectangle) { return new Rectangle2D(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); @@ -95,10 +106,10 @@ public boolean queryContainsPoint(int x, int y) { } /** compare this to a provided rangle bounding box **/ - public PointValues.Relation relateRangeBBox(int minXOffset, int minYOffset, byte[] minTriangle, - int maxXOffset, int maxYOffset, byte[] maxTriangle) { - PointValues.Relation eastRelation = compareBBoxToRangeBBox(this.bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle); - if (this.crossesDateline() && eastRelation == PointValues.Relation.CELL_OUTSIDE_QUERY) { + public Relation relateRangeBBox(int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle) { + Relation eastRelation = compareBBoxToRangeBBox(this.bbox, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle); + if (this.crossesDateline() && eastRelation == Relation.CELL_OUTSIDE_QUERY) { return compareBBoxToRangeBBox(this.west, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle); } return eastRelation; @@ -155,24 +166,24 @@ public boolean containsTriangle(int ax, int ay, int bx, int by, int cx, int cy) } /** 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, - int minXOffset, int minYOffset, byte[] minTriangle, - int maxXOffset, int maxYOffset, byte[] maxTriangle) { + private static Relation compareBBoxToRangeBBox(final byte[] bbox, + int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle) { // check bounding box (DISJOINT) 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(minTriangle, minYOffset, minYOffset + BYTES, bbox, 2 * BYTES, 3 * 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 && Arrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) <= 0 && Arrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, 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 @@ private static boolean boxesAreDisjoint(final int aMinX, final int aMaxX, final final int bMinX, final int bMaxX, final int bMinY, final int 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; + } } diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/Tessellator.java b/lucene/sandbox/src/java/org/apache/lucene/geo/Tessellator.java index 9e96a047ff41..e46df18af1ae 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/geo/Tessellator.java +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/Tessellator.java @@ -80,11 +80,51 @@ private enum State { // No Instance: private Tessellator() {} - /** Produces an array of vertices representing the triangulated result set of the Points array */ public static final List tessellate(final Polygon 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, 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 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 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(outerNode == null) { throw new IllegalArgumentException("Malformed shape detected in Tessellator!"); @@ -120,16 +160,17 @@ public static final List tessellate(final Polygon polygon) { } /** 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; // Link points into the circular doubly-linked list in the specified winding order - if (windingOrder == polygon.getWindingOrder()) { - for (int i = 0; i < polygon.numPoints(); ++i) { - lastNode = insertNode(polygon, startIndex++, i, lastNode); + if (windingOrder == polyWindingOrder) { + for (int i = 0; i < x.length; ++i) { + lastNode = insertNode(x, y, startIndex++, i, lastNode, isGeo); } } else { - for (int i = polygon.numPoints() - 1; i >= 0; --i) { - lastNode = insertNode(polygon, startIndex++, i, lastNode); + for (int i = x.length - 1; i >= 0; --i) { + 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 @@ -142,6 +183,29 @@ private static final Node createDoublyLinkedList(final Polygon polygon, int star 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 holeList = new ArrayList<>(); + // keep a reference to the hole + final Map 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. **/ private static final Node eliminateHoles(final Polygon polygon, Node outerNode) { // Define a list to hole a reference to each filtered hole list. @@ -153,8 +217,7 @@ private static final Node eliminateHoles(final Polygon polygon, Node outerNode) int nodeIndex = polygon.numPoints(); for(int i = 0; i < polygon.numHoles(); ++i) { // 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) { throw new IllegalArgumentException("Points are all coplanar in hole: " + holes[i]); } @@ -167,7 +230,10 @@ private static final Node eliminateHoles(final Polygon polygon, Node outerNode) } nodeIndex += holes[i].numPoints(); } + return eliminateHoles(holeList, holeListPolygons, outerNode); + } + private static final Node eliminateHoles(List holeList, final Map holeListPolygons, Node outerNode) { // Sort the hole vertices by x coordinate holeList.sort((Node pNodeA, Node pNodeB) -> { @@ -188,8 +254,22 @@ private static final Node eliminateHoles(final Polygon polygon, Node outerNode) for(int i = 0; i < holeList.size(); ++i) { // Eliminate hole triangles from the result set final Node holeNode = holeList.get(i); - final Polygon hole = holeListPolygons.get(holeNode); - eliminateHole(holeNode, outerNode, hole); + double holeMinX, holeMaxX, holeMinY, holeMaxY; + 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. outerNode = filterPoints(outerNode, outerNode.next); } @@ -198,11 +278,11 @@ private static final Node eliminateHoles(final Polygon polygon, Node outerNode) } /** 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. Node next = outerNode; 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); if (sharedVertex != null) { // Split the resulting polygon. @@ -319,7 +399,7 @@ private static final Node fetchLeftmost(final Node start) { } /** Main ear slicing loop which triangulates the vertices of a polygon, provided as a doubly-linked list. **/ - private static final List earcutLinkedList(Polygon polygon, Node currEar, final List tessellation, + private static final List earcutLinkedList(Object polygon, Node currEar, final List tessellation, State state, final boolean mortonOptimized) { earcut : do { if (currEar == null || currEar.previous == currEar.next) { @@ -479,7 +559,7 @@ && isLocallyInside(a, b) && isLocallyInside(b, a)) { } /** 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 tessellation, final boolean mortonIndexed) { + private static final boolean splitEarcut(Object polygon, final Node start, final List tessellation, final boolean mortonIndexed) { // Search for a valid diagonal that divides the polygon into two. Node searchNode = start; Node nextNode; @@ -553,7 +633,7 @@ private static boolean isCWPolygon(Node start, Node end) { double windingSum = 0; do { // 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; } while (next.next != end); //The polygon must be CW @@ -734,8 +814,8 @@ private static final Node filterPoints(final Node start, Node end) { } /** 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) { - final Node node = new Node(polygon, index, vertexIndex); + 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(x, y, index, vertexIndex, isGeo); if(lastNode == null) { node.previous = node; node.previousZ = node; @@ -822,7 +902,9 @@ protected static class Node { // vertex index in the polygon private final int vrtxIdx; // 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 private final int x; // encoded y value @@ -839,13 +921,14 @@ protected static class Node { // next z node 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.vrtxIdx = vertexIndex; - this.polygon = polygon; - this.y = encodeLatitude(polygon.getPolyLat(vrtxIdx)); - this.x = encodeLongitude(polygon.getPolyLon(vrtxIdx)); - this.morton = BitUtil.interleave(x ^ 0x80000000, y ^ 0x80000000); + this.polyX = x; + this.polyY = y; + this.y = isGeo ? encodeLatitude(polyY[vrtxIdx]) : XYEncodingUtils.encode(polyY[vrtxIdx]); + this.x = isGeo ? encodeLongitude(polyX[vrtxIdx]) : XYEncodingUtils.encode(polyX[vrtxIdx]); + this.morton = BitUtil.interleave(this.x ^ 0x80000000, this.y ^ 0x80000000); this.previous = null; this.next = null; this.previousZ = null; @@ -856,7 +939,8 @@ protected Node(final Polygon polygon, final int index, final int vertexIndex) { protected Node(Node other) { this.idx = other.idx; this.vrtxIdx = other.vrtxIdx; - this.polygon = other.polygon; + this.polyX = other.polyX; + this.polyY = other.polyY; this.morton = other.morton; this.x = other.x; this.y = other.y; @@ -868,22 +952,12 @@ protected Node(Node other) { /** get the x value */ public final double getX() { - return polygon.getPolyLon(vrtxIdx); + return polyX[vrtxIdx]; } /** get the y value */ public final double getY() { - return polygon.getPolyLat(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); + return polyY[vrtxIdx]; } @Override @@ -920,22 +994,22 @@ public int getEncodedY(int vertex) { return this.vertex[vertex].y; } - /** get latitude value for the given vertex */ - public double getLat(int vertex) { - return this.vertex[vertex].getLat(); + /** get y value for the given vertex */ + public double getY(int vertex) { + return this.vertex[vertex].getY(); } - /** get longitude value for the given vertex */ - public double getLon(int vertex) { - return this.vertex[vertex].getLon(); + /** get x value for the given vertex */ + public double getX(int vertex) { + return this.vertex[vertex].getX(); } /** utility method to compute whether the point is in the triangle */ protected boolean containsPoint(double lat, double lon) { return pointInTriangle(lon, lat, - vertex[0].getLon(), vertex[0].getLat(), - vertex[1].getLon(), vertex[1].getLat(), - vertex[2].getLon(), vertex[2].getLat()); + vertex[0].getX(), vertex[0].getY(), + vertex[1].getX(), vertex[1].getY(), + vertex[2].getX(), vertex[2].getY()); } /** pretty print the triangle vertices */ diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/XYEncodingUtils.java b/lucene/sandbox/src/java/org/apache/lucene/geo/XYEncodingUtils.java new file mode 100644 index 000000000000..025504fd1e85 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/XYEncodingUtils.java @@ -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)); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/XYLine.java b/lucene/sandbox/src/java/org/apache/lucene/geo/XYLine.java new file mode 100644 index 000000000000..9ece6594c3d1 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/XYLine.java @@ -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(); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/XYPolygon.java b/lucene/sandbox/src/java/org/apache/lucene/geo/XYPolygon.java new file mode 100644 index 000000000000..585283d1a703 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/XYPolygon.java @@ -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(); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/XYPolygon2D.java b/lucene/sandbox/src/java/org/apache/lucene/geo/XYPolygon2D.java new file mode 100644 index 000000000000..1dba72bb11b8 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/XYPolygon2D.java @@ -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); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/XYRectangle.java b/lucene/sandbox/src/java/org/apache/lucene/geo/XYRectangle.java new file mode 100644 index 000000000000..4751cfc163d3 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/XYRectangle.java @@ -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(); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/XYRectangle2D.java b/lucene/sandbox/src/java/org/apache/lucene/geo/XYRectangle2D.java new file mode 100644 index 000000000000..a1259166a34d --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/XYRectangle2D.java @@ -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(); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java index fc7e290a0267..b74bfccaef0f 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java @@ -16,43 +16,19 @@ */ 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.LatLonShape.QueryRelation; +import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon2D; 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.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.decodeLongitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; @@ -62,13 +38,8 @@ import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; -/** base test class for {@link TestLatLonLineShapeQueries}, {@link TestLatLonPointShapeQueries}, - * and {@link TestLatLonPolygonShapeQueries} */ -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}; +/** Base test case for testing geospatial indexing and search functionality **/ +public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase { protected abstract ShapeType getShapeType(); @@ -76,98 +47,75 @@ protected Object nextShape() { return getShapeType().nextShape(); } - /** quantizes a latitude value to be consistent with index encoding */ - protected static double quantizeLat(double rawLat) { - return decodeLatitude(encodeLatitude(rawLat)); + /** factory method to create a new bounding box query */ + @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); } - /** quantizes a provided latitude value rounded up to the nearest encoded integer */ - protected static double quantizeLatCeil(double rawLat) { - return decodeLatitude(encodeLatitudeCeil(rawLat)); + /** 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)); } - /** quantizes a longitude value to be consistent with index encoding */ - protected static double quantizeLon(double rawLon) { - return decodeLongitude(encodeLongitude(rawLon)); + /** 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)); } - /** quantizes a provided longitude value rounded up to the nearest encoded integer */ - protected static double quantizeLonCeil(double rawLon) { - return decodeLongitude(encodeLongitudeCeil(rawLon)); + @Override + protected Line2D toLine2D(Object... lines) { + return Line2D.create(Arrays.stream(lines).toArray(Line[]::new)); } - /** 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])}; + @Override + protected Polygon2D toPolygon2D(Object... polygons) { + return Polygon2D.create(Arrays.stream(polygons).toArray(Polygon[]::new)); } - /** 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; + @Override + public Rectangle randomQueryBox() { + return GeoTestUtil.nextBox(); } - /** 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); + @Override + protected double rectMinX(Object rect) { + return ((Rectangle)rect).minLon; } - /** - * 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(); + @Override + protected double rectMaxX(Object rect) { + return ((Rectangle)rect).maxLon; } - /** 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 */ - protected Query newRectQuery(String field, QueryRelation queryRelation, double minLat, double maxLat, double minLon, double maxLon) { - return LatLonShape.newBoxQuery(field, queryRelation, minLat, maxLat, minLon, maxLon); + @Override + protected double rectMinY(Object rect) { + return ((Rectangle)rect).minLat; } public void testBoxQueryEqualsAndHashcode() { Rectangle rectangle = GeoTestUtil.nextBox(); QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values()); String fieldName = "foo"; - Query q1 = newRectQuery(fieldName, queryRelation, rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); - Query q2 = 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.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat); QueryUtils.checkEqual(q1, q2); //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); //different query relation 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) { QueryUtils.checkEqual(q1, q4); } else { QueryUtils.checkUnequal(q1, q4); } //different shape - Rectangle newRectangle = GeoTestUtil.nextBox();; - Query q5 = newRectQuery(fieldName, queryRelation, newRectangle.minLat, newRectangle.maxLat, newRectangle.minLon, newRectangle.maxLon); + Rectangle newRectangle = GeoTestUtil.nextBox(); + Query q5 = newRectQuery(fieldName, queryRelation, rectangle.minLon, rectangle.maxLon, newRectangle.minLat, newRectangle.maxLat); if (rectangle.equals(newRectangle)) { QueryUtils.checkEqual(q1, q5); } else { @@ -241,458 +189,102 @@ public void testPolygonQueryEqualsAndHashcode() { } } - // 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); + @Override + protected double rectMaxY(Object rect) { + return ((Rectangle)rect).maxLat; } - @Slow - public void testRandomMedium() throws Exception { - doTestRandom(1000); + @Override + protected boolean rectCrossesDateline(Object rect) { + return ((Rectangle)rect).crossesDateline(); } - @Slow - @Nightly - public void testRandomBig() throws Exception { - doTestRandom(20000); + /** use {@link GeoTestUtil#nextPolygon()} to create a random line; TODO: move to GeoTestUtil */ + @Override + public Line nextLine() { + return getNextLine(); } - protected void doTestRandom(int count) throws Exception { - int numShapes = atLeast(count); - ShapeType type = getShapeType(); - - if (VERBOSE) { - System.out.println("TEST: number of " + type.name() + " shapes=" + numShapes); - } + public static Line getNextLine() { + 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); - 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); + return new Line(lats, lons); } - 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); + @Override + protected Polygon nextPolygon() { + return GeoTestUtil.nextPolygon(); } - protected void indexRandomShapes(IndexWriter w, Object... shapes) throws Exception { - Set 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]); + @Override + protected Encoder getEncoder() { + return new Encoder() { + @Override + double quantizeX(double raw) { + return decodeLongitude(encodeLongitude(raw)); } - 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); + @Override + double quantizeXCeil(double raw) { + return decodeLongitude(encodeLongitudeCeil(raw)); } - // BBox - Rectangle rect = GeoTestUtil.nextBox(); - QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values()); - Query query = newRectQuery(FIELD_NAME, queryRelation, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon); - - if (VERBOSE) { - System.out.println(" query=" + query + ", relation=" + queryRelation); + @Override + double quantizeY(double raw) { + return decodeLatitude(encodeLatitude(raw)); } - 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 = 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"); + @Override + double quantizeYCeil(double raw) { + return decodeLatitude(encodeLatitudeCeil(raw)); } - } - } - 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); + /** quantizes a latitude value to be consistent with index encoding */ + protected double quantizeLat(double rawLat) { + return quantizeY(rawLat); } - // line - Line queryLine = randomQueryLine(shapes); - Line2D queryLine2D = Line2D.create(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); + /** quantizes a provided latitude value rounded up to the nearest encoded integer */ + protected double quantizeLatCeil(double rawLat) { + return quantizeYCeil(rawLat); } - 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 = 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"); + /** quantizes a longitude value to be consistent with index encoding */ + protected double quantizeLon(double rawLon) { + return quantizeX(rawLon); } - } - } - - /** 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); + /** quantizes a provided longitude value rounded up to the nearest encoded integer */ + protected double quantizeLonCeil(double rawLon) { + return quantizeXCeil(rawLon); } - // Polygon - Polygon queryPolygon = GeoTestUtil.nextPolygon(); - Polygon2D queryPoly2D = Polygon2D.create(queryPolygon); - QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values()); - Query query = newPolygonQuery(FIELD_NAME, queryRelation, queryPolygon); - - if (VERBOSE) { - System.out.println(" query=" + query + ", relation=" + queryRelation); + @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])}; } - 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 = 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; - } - } + @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, encodeLatitude(ay), encodeLongitude(ax), encodeLatitude(by), encodeLongitude(bx), encodeLatitude(cy), encodeLongitude(cx)); + int[] decoded = new int[6]; + ShapeField.decodeTriangle(encoded, decoded); + return decoded; } - if (fail) { - fail("some hits were wrong"); - } - } - } - - protected abstract Validator getValidator(QueryRelation relation); - - /** internal point class for testing point shapes */ - protected static class Point { - double lat; - double lon; - - public Point(double lat, double lon) { - this.lat = lat; - 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 */ protected enum ShapeType { POINT() { public Point nextShape() { - return new Point(nextLatitude(), nextLongitude()); + return new Point(nextLongitude(), nextLatitude()); } }, LINE() { @@ -737,15 +329,25 @@ static ShapeType fromObject(Object shape) { } } - /** validator class used to test query results against "ground truth" */ - protected static abstract class Validator { - 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(Polygon2D poly2d, Object shape); + /** internal lat lon point class for testing point shapes */ + protected static class Point { + double lon; + double lat; + + public Point(double lon, double lat) { + this.lon = lon; + this.lat = lat; + } - public void setRelation(QueryRelation relation) { - this.queryRelation = relation; + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("POINT("); + sb.append(lon); + sb.append(','); + sb.append(lat); + sb.append(')'); + return sb.toString(); } } + } diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeEncodingTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeEncodingTestCase.java new file mode 100644 index 000000000000..daa9bacb3800 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeEncodingTestCase.java @@ -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); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeTestCase.java new file mode 100644 index 000000000000..1e4e173dcdd4 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/BaseShapeTestCase.java @@ -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 + *

    + * 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 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; + } + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java new file mode 100644 index 000000000000..b706b5f20cdc --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java @@ -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(); + } + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java index 7e6d9958ace5..d7ed52946d1d 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java @@ -18,7 +18,7 @@ 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.GeoTestUtil; import org.apache.lucene.geo.Line; @@ -28,12 +28,10 @@ import org.apache.lucene.geo.Rectangle2D; 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") public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase { - protected final LineValidator VALIDATOR = new LineValidator(); - @Override protected ShapeType getShapeType() { return ShapeType.LINE; @@ -71,18 +69,21 @@ protected Field[] createIndexableFields(String field, Object line) { } @Override - protected Validator getValidator(QueryRelation queryRelation) { - VALIDATOR.setRelation(queryRelation); - return VALIDATOR; + 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 minLat, double maxLat, double minLon, double maxLon, Object shape) { Line line = (Line)shape; Rectangle2D rectangle2D = Rectangle2D.create(new Rectangle(minLat, maxLat, minLon, maxLon)); 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 (rectangle2D.containsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == false) { return false; @@ -102,14 +103,14 @@ public boolean testLineQuery(Line2D line2d, Object shape) { } @Override - public boolean testPolygonQuery(Polygon2D poly2d, Object shape) { - return testLine(poly2d, (Line) shape); + public boolean testPolygonQuery(Object poly2d, Object shape) { + return testLine((Polygon2D)poly2d, (Line) shape); } private boolean testLine(EdgeTree queryPoly, Line line) { 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]); if (queryRelation == QueryRelation.DISJOINT) { if (r != Relation.CELL_OUTSIDE_QUERY) return false; diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiLineShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiLineShapeQueries.java index 5c9c42ee330c..fc5bc91247bc 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiLineShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiLineShapeQueries.java @@ -19,17 +19,13 @@ import java.util.ArrayList; 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.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 { - protected final MultiLineValidator VALIDATOR = new MultiLineValidator(); - protected final TestLatLonLineShapeQueries.LineValidator LINEVALIDATOR = new TestLatLonLineShapeQueries.LineValidator(); - @Override protected ShapeType getShapeType() { return ShapeType.LINE; @@ -59,13 +55,24 @@ protected Field[] createIndexableFields(String name, Object o) { } @Override - protected Validator getValidator(QueryRelation relation) { - VALIDATOR.setRelation(relation); - LINEVALIDATOR.setRelation(relation); - return VALIDATOR; + public Validator getValidator() { + return new MultiLineValidator(ENCODER); } 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 public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { Line[] lines = (Line[])shape; @@ -99,7 +106,7 @@ public boolean testLineQuery(Line2D query, Object shape) { } @Override - public boolean testPolygonQuery(Polygon2D query, Object shape) { + public boolean testPolygonQuery(Object query, Object shape) { Line[] lines = (Line[])shape; for (Line l : lines) { boolean b = LINEVALIDATOR.testPolygonQuery(query, l); diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiPointShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiPointShapeQueries.java index 44d095d2d1b0..536e9c2178ec 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiPointShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiPointShapeQueries.java @@ -19,17 +19,12 @@ import java.util.ArrayList; import java.util.List; -import org.apache.lucene.document.LatLonShape.QueryRelation; -import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.document.ShapeField.QueryRelation; 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 { - protected final MultiPointValidator VALIDATOR = new MultiPointValidator(); - protected final TestLatLonPointShapeQueries.PointValidator POINTVALIDATOR = new TestLatLonPointShapeQueries.PointValidator(); - @Override protected ShapeType getShapeType() { return ShapeType.POINT; @@ -40,7 +35,7 @@ protected Point[] nextShape() { int n = random().nextInt(4) + 1; Point[] points = new Point[n]; for (int i =0; i < n; i++) { - points[i] = new Point(GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude()); + points[i] = (Point)ShapeType.POINT.nextShape(); } return points; } @@ -59,13 +54,24 @@ protected Field[] createIndexableFields(String name, Object o) { } @Override - protected Validator getValidator(QueryRelation relation) { - VALIDATOR.setRelation(relation); - POINTVALIDATOR.setRelation(relation); - return VALIDATOR; + public Validator getValidator() { + return new MultiPointValidator(ENCODER); } 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 public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { Point[] points = (Point[]) shape; @@ -99,7 +105,7 @@ public boolean testLineQuery(Line2D query, Object shape) { } @Override - public boolean testPolygonQuery(Polygon2D query, Object shape) { + public boolean testPolygonQuery(Object query, Object shape) { Point[] points = (Point[]) shape; for (Point p : points) { boolean b = POINTVALIDATOR.testPolygonQuery(query, p); diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiPolygonShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiPolygonShapeQueries.java index 3729bbad08b5..bba9f97a5493 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiPolygonShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonMultiPolygonShapeQueries.java @@ -19,18 +19,14 @@ import java.util.ArrayList; 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.Polygon; -import org.apache.lucene.geo.Polygon2D; 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 { - protected final MultiPolygonValidator VALIDATOR = new MultiPolygonValidator(); - protected final TestLatLonPolygonShapeQueries.PolygonValidator POLYGONVALIDATOR = new TestLatLonPolygonShapeQueries.PolygonValidator(); - @Override protected ShapeType getShapeType() { return ShapeType.POLYGON; @@ -71,13 +67,24 @@ protected Field[] createIndexableFields(String name, Object o) { } @Override - protected Validator getValidator(QueryRelation relation) { - VALIDATOR.setRelation(relation); - POLYGONVALIDATOR.setRelation(relation); - return VALIDATOR; + protected Validator getValidator() { + return new MultiPolygonValidator(ENCODER); } 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 public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) { Polygon[] polygons = (Polygon[])shape; @@ -111,7 +118,7 @@ public boolean testLineQuery(Line2D query, Object shape) { } @Override - public boolean testPolygonQuery(Polygon2D query, Object shape) { + public boolean testPolygonQuery(Object query, Object shape) { Polygon[] polygons = (Polygon[])shape; for (Polygon p : polygons) { boolean b = POLYGONVALIDATOR.testPolygonQuery(query, p); diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java index d894aed7c9a7..b3ab59b9b60b 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java @@ -17,7 +17,7 @@ package org.apache.lucene.document; 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.GeoTestUtil; import org.apache.lucene.geo.Line; @@ -25,11 +25,9 @@ import org.apache.lucene.geo.Polygon2D; 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 { - protected final PointValidator VALIDATOR = new PointValidator(); - @Override protected ShapeType getShapeType() { return ShapeType.POINT; @@ -67,17 +65,20 @@ protected Field[] createIndexableFields(String field, Object point) { } @Override - protected Validator getValidator(QueryRelation relation) { - VALIDATOR.setRelation(relation); - return VALIDATOR; + 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 = quantizeLat(p.lat); - double lon = quantizeLon(p.lon); + double lat = encoder.quantizeY(p.lat); + double lon = encoder.quantizeX(p.lon); boolean isDisjoint = lat < minLat || lat > maxLat; isDisjoint = isDisjoint || ((minLon > maxLon) @@ -95,13 +96,13 @@ public boolean testLineQuery(Line2D line2d, Object shape) { } @Override - public boolean testPolygonQuery(Polygon2D poly2d, Object shape) { - return testPoint(poly2d, (Point) shape); + public boolean testPolygonQuery(Object poly2d, Object shape) { + return testPoint((Polygon2D)poly2d, (Point) shape); } private boolean testPoint(EdgeTree tree, Point p) { - double lat = quantizeLat(p.lat); - double lon = quantizeLon(p.lon); + double lat = encoder.quantizeY(p.lat); + double lon = encoder.quantizeX(p.lon); // 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) { diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java index a3e060c9ae11..8b3cab4edda5 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java @@ -18,7 +18,7 @@ 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.Line2D; import org.apache.lucene.geo.Polygon; @@ -28,11 +28,9 @@ import org.apache.lucene.geo.Tessellator; 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 { - protected final PolygonValidator VALIDATOR = new PolygonValidator(); - @Override protected ShapeType getShapeType() { return ShapeType.POLYGON; @@ -59,19 +57,22 @@ protected Field[] createIndexableFields(String field, Object polygon) { } @Override - protected Validator getValidator(QueryRelation relation) { - VALIDATOR.setRelation(relation); - return VALIDATOR; + 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 minLat, double maxLat, double minLon, double maxLon, Object shape) { Polygon p = (Polygon)shape; Rectangle2D rectangle2D = Rectangle2D.create(new Rectangle(minLat, maxLat, minLon, maxLon)); List tessellation = Tessellator.tessellate(p); 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 (rectangle2D.containsTriangle(decoded[1], decoded[0], decoded[3], decoded[2], decoded[5], decoded[4]) == false) { return false; @@ -91,14 +92,14 @@ public boolean testLineQuery(Line2D query, Object shape) { } @Override - public boolean testPolygonQuery(Polygon2D query, Object shape) { - return testPolygon(query, (Polygon) shape); + public boolean testPolygonQuery(Object query, Object shape) { + return testPolygon((Polygon2D)query, (Polygon) shape); } private boolean testPolygon(EdgeTree tree, Polygon shape) { List tessellation = Tessellator.tessellate(shape); 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]); if (queryRelation == QueryRelation.DISJOINT) { if (r != Relation.CELL_OUTSIDE_QUERY) return false; diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShape.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShape.java index a325bc760e22..66948a42ebc7 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShape.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShape.java @@ -17,7 +17,7 @@ package org.apache.lucene.document; 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.Line; import org.apache.lucene.geo.Line2D; @@ -235,11 +235,11 @@ public void testLUCENE8454() throws Exception { Tessellator.Triangle t = Tessellator.tessellate(poly).get(0); - byte[] encoded = new byte[7 * LatLonShape.BYTES]; - LatLonShape.encodeTriangle(encoded, encodeLatitude(t.getLat(0)), encodeLongitude(t.getLon(0)), - encodeLatitude(t.getLat(1)), encodeLongitude(t.getLon(1)), encodeLatitude(t.getLat(2)), encodeLongitude(t.getLon(2))); + byte[] encoded = new byte[7 * ShapeField.BYTES]; + ShapeField.encodeTriangle(encoded, encodeLatitude(t.getY(0)), encodeLongitude(t.getX(0)), + encodeLatitude(t.getY(1)), encodeLongitude(t.getX(1)), encodeLatitude(t.getY(2)), encodeLongitude(t.getX(2))); 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; diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShapeEncoding.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShapeEncoding.java index e4cd2bc5e537..22700f5ffa33 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShapeEncoding.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShapeEncoding.java @@ -16,546 +16,51 @@ */ package org.apache.lucene.document; -import java.util.Arrays; - - import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoTestUtil; -import org.apache.lucene.geo.GeoUtils; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon2D; -import org.apache.lucene.index.PointValues; -import org.apache.lucene.util.LuceneTestCase; /** Test case for LatLonShape encoding */ -public class TestLatLonShapeEncoding extends LuceneTestCase { - - //One shared point with MBR -> MinLat, MinLon - public void testPolygonEncodingMinLatMinLon() { - double alat = 0.0; - 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 - public void testPolygonEncodingMinLatMaxLon() { - double alat = 1.0; - 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 - public void testPolygonEncodingMaxLatMaxLon() { - double alat = 1.0; - 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); - } +public class TestLatLonShapeEncoding extends BaseShapeEncodingTestCase { - //One shared point with MBR -> MaxLat, MinLon - public void testPolygonEncodingMaxLatMinLon() { - double alat = 2.0; - 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); + @Override + protected int encodeX(double x) { + return GeoEncodingUtils.encodeLongitude(x); } - //Two shared point with MBR -> [MinLat, MinLon], [MaxLat, MaxLon], third point below - public void testPolygonEncodingMinLatMinLonMaxLatMaxLonBelow() { - double alat = 0.0; - 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); + @Override + protected int encodeY(double y) { + return GeoEncodingUtils.encodeLatitude(y); } - //Two shared point with MBR -> [MinLat, MinLon], [MaxLat, MaxLon], third point above - public void testPolygonEncodingMinLatMinLonMaxLatMaxLonAbove() { - double alat = 0.0; - 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); + @Override + protected double decodeX(int xEncoded) { + return GeoEncodingUtils.decodeLongitude(xEncoded); } - //Two shared point with MBR -> [MinLat, MaxLon], [MaxLat, MinLon], third point below - public void testPolygonEncodingMinLatMaxLonMaxLatMinLonBelow() { - double alat = 2.0; - 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); + @Override + protected double decodeY(int yEncoded) { + return GeoEncodingUtils.decodeLatitude(yEncoded); } - //Two shared point with MBR -> [MinLat, MaxLon], [MaxLat, MinLon], third point above - public void testPolygonEncodingMinLatMaxLonMaxLatMinLonAbove() { - double alat = 2.0; - 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); + @Override + protected double nextX() { + return GeoTestUtil.nextLongitude(); } - //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); + @Override + protected double nextY() { + return GeoTestUtil.nextLatitude(); } - //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); - } + @Override + protected Polygon nextPolygon() { + return GeoTestUtil.nextPolygon(); } - 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); + @Override + protected Polygon2D createPolygon2D(Object polygon) { + return Polygon2D.create((Polygon)polygon); } } diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYLineShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYLineShapeQueries.java new file mode 100644 index 000000000000..c66b9d1e0792 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYLineShapeQueries.java @@ -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; + } + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYMultiLineShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYMultiLineShapeQueries.java new file mode 100644 index 000000000000..b2978e58da63 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYMultiLineShapeQueries.java @@ -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 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); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYMultiPointShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYMultiPointShapeQueries.java new file mode 100644 index 000000000000..54de8abd5fab --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYMultiPointShapeQueries.java @@ -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 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); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYMultiPolygonShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYMultiPolygonShapeQueries.java new file mode 100644 index 000000000000..551919661020 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYMultiPolygonShapeQueries.java @@ -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 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); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPointShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPointShapeQueries.java new file mode 100644 index 000000000000..edeefa54a59a --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPointShapeQueries.java @@ -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; + } + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPolygonShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPolygonShapeQueries.java new file mode 100644 index 000000000000..82d887aa2582 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYPolygonShapeQueries.java @@ -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 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 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); + } + +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYShape.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYShape.java new file mode 100644 index 000000000000..1fc1a79282eb --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYShape.java @@ -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); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestXYShapeEncoding.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYShapeEncoding.java new file mode 100644 index 000000000000..62d53924e1b9 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestXYShapeEncoding.java @@ -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); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/geo/ShapeTestUtil.java b/lucene/sandbox/src/test/org/apache/lucene/geo/ShapeTestUtil.java new file mode 100644 index 000000000000..f7e304bc5200 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/geo/ShapeTestUtil.java @@ -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 xList = new ArrayList<>(); + ArrayList 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 triangles) { double area = 0; for (Tessellator.Triangle t : triangles) { - double[] lats = new double[] {t.getLat(0), t.getLat(1), t.getLat(2), t.getLat(0)}; - double[] lons = new double[] {t.getLon(0), t.getLon(1), t.getLon(2), t.getLon(0)}; + double[] lats = new double[] {t.getY(0), t.getY(1), t.getY(2), t.getY(0)}; + double[] lons = new double[] {t.getX(0), t.getX(1), t.getX(2), t.getX(0)}; area += area(new Polygon(lats, lons)); } return area;