Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LUCENE-8973: XYRectangle2D should work on float space #865

Closed
wants to merge 15 commits into from
Expand Up @@ -20,6 +20,9 @@
import org.apache.lucene.geo.XYRectangle;
import org.apache.lucene.geo.XYRectangle2D;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.util.NumericUtils;

import static org.apache.lucene.geo.XYEncodingUtils.decode;

/**
* Finds all previously indexed cartesian shapes that intersect the specified bounding box.
Expand All @@ -41,7 +44,13 @@ public XYShapeBoundingBoxQuery(String field, QueryRelation queryRelation, double
@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);
float minY = (float) decode(NumericUtils.sortableBytesToInt(minTriangle, minYOffset));
float minX = (float) decode(NumericUtils.sortableBytesToInt(minTriangle, minXOffset));
float maxY = (float) decode(NumericUtils.sortableBytesToInt(maxTriangle, maxYOffset));
float maxX = (float) decode(NumericUtils.sortableBytesToInt(maxTriangle, maxXOffset));

// check internal node against query
return rectangle2D.relate(minX, maxX, minY, maxY);
}

/** returns true if the query matches the encoded triangle */
Expand All @@ -50,17 +59,17 @@ protected boolean queryMatches(byte[] t, ShapeField.DecodedTriangle scratchTrian
// decode indexed triangle
ShapeField.decodeTriangle(t, scratchTriangle);

int aY = scratchTriangle.aY;
int aX = scratchTriangle.aX;
int bY = scratchTriangle.bY;
int bX = scratchTriangle.bX;
int cY = scratchTriangle.cY;
int cX = scratchTriangle.cX;
float aY = (float) decode(scratchTriangle.aY);
float aX = (float) decode(scratchTriangle.aX);
float bY = (float) decode(scratchTriangle.bY);
float bX = (float) decode(scratchTriangle.bX);
float cY = (float) decode(scratchTriangle.cY);
float cX = (float) decode(scratchTriangle.cX);

switch (queryRelation) {
case INTERSECTS: return rectangle2D.intersectsTriangle(aX, aY, bX, bY, cX, cY);
case WITHIN: return rectangle2D.containsTriangle(aX, aY, bX, bY, cX, cY);
case DISJOINT: return rectangle2D.intersectsTriangle(aX, aY, bX, bY, cX, cY) == false;
case INTERSECTS: return rectangle2D.relateTriangle(aX, aY, bX, bY, cX, cY) != PointValues.Relation.CELL_OUTSIDE_QUERY;
case WITHIN: return rectangle2D.contains(aX, aY) && rectangle2D.contains(bX, bY) && rectangle2D.contains(cX, cY);
case DISJOINT: return rectangle2D.relateTriangle(aX, aY, bX, bY, cX, cY) == PointValues.Relation.CELL_OUTSIDE_QUERY;
default: throw new IllegalArgumentException("Unsupported query type :[" + queryRelation + "]");
}
}
Expand Down
142 changes: 128 additions & 14 deletions lucene/sandbox/src/java/org/apache/lucene/geo/XYRectangle2D.java
Expand Up @@ -16,42 +16,156 @@
*/
package org.apache.lucene.geo;

import static org.apache.lucene.geo.XYEncodingUtils.decode;
import static org.apache.lucene.geo.XYEncodingUtils.encode;
import java.util.Arrays;
import java.util.Objects;

import org.apache.lucene.index.PointValues;

import static org.apache.lucene.geo.GeoUtils.orient;

/**
* 2D rectangle implementation containing cartesian spatial logic.
*
* @lucene.internal
*/
public class XYRectangle2D extends Rectangle2D {
public class XYRectangle2D {

protected XYRectangle2D(double minX, double maxX, double minY, double maxY) {
super(encode(minX), encode(maxX), encode(minY), encode(maxY));
private final float minX;
private final float maxX;
private final float minY;
private final float maxY;

protected XYRectangle2D(float minX, float maxX, float minY, float maxY) {
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
}

/** Builds a Rectangle2D from rectangle */
public static XYRectangle2D create(XYRectangle rectangle) {
return new XYRectangle2D(rectangle.minX, rectangle.maxX, rectangle.minY, rectangle.maxY);
public boolean contains(float x, float y) {
return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY;
}

@Override
public boolean crossesDateline() {
public PointValues.Relation relate(float minX, float maxX, float minY, float maxY) {
if (this.minX > maxX || this.maxX < minX || this.minY > maxY || this.maxY < minY) {
return PointValues.Relation.CELL_OUTSIDE_QUERY;
}
if (minX >= this.minX && maxX <= this.maxX && minY >= this.minY && maxY <= this.maxY) {
return PointValues.Relation.CELL_INSIDE_QUERY;
}
return PointValues.Relation.CELL_CROSSES_QUERY;
}

public PointValues.Relation relateTriangle(float aX, float aY, float bX, float bY, float cX, float cY) {
// compute bounding box of triangle
float tMinX = StrictMath.min(StrictMath.min(aX, bX), cX);
float tMaxX = StrictMath.max(StrictMath.max(aX, bX), cX);
float tMinY = StrictMath.min(StrictMath.min(aY, bY), cY);
float tMaxY = StrictMath.max(StrictMath.max(aY, bY), cY);

if (tMaxX < minX || tMinX > maxX || tMinY > maxY || tMaxY < minY) {
return PointValues.Relation.CELL_OUTSIDE_QUERY;
}

int edgesContain = numberOfCorners(aX, aY, bX, bY, cX, cY);
if (edgesContain == 3) {
return PointValues.Relation.CELL_INSIDE_QUERY;
} else if (edgesContain != 0) {
return PointValues.Relation.CELL_CROSSES_QUERY;
} else if (Tessellator.pointInTriangle(minX, minY, aX, aY, bX, bY, cX, cY)
|| edgesIntersect(aX, aY, bX, bY)
|| edgesIntersect(bX, bY, cX, cY)
|| edgesIntersect(cX, cY, aX, aY)) {
return PointValues.Relation.CELL_CROSSES_QUERY;
}
return PointValues.Relation.CELL_OUTSIDE_QUERY;
}

private boolean edgesIntersect(float ax, float ay, float bx, float by) {
// shortcut: if edge is a point (occurs w/ Line shapes); simply check bbox w/ point
if (ax == bx && ay == by) {
return false;
}

// shortcut: check bboxes of edges are disjoint
if ( Math.max(ax, bx) < minX || Math.min(ax, bx) > maxX || Math.min(ay, by) > maxY || Math.max(ay, by) < minY) {
return false;
}

// top
if (orient(ax, ay, bx, by, minX, maxY) * orient(ax, ay, bx, by, maxX, maxY) <= 0 &&
orient(minX, maxY, maxX, maxY, ax, ay) * orient(minX, maxY, maxX, maxY, bx, by) <= 0) {
return true;
}

// right
if (orient(ax, ay, bx, by, maxX, maxY) * orient(ax, ay, bx, by, maxX, minY) <= 0 &&
orient(maxX, maxY, maxX, minY, ax, ay) * orient(maxX, maxY, maxX, minY, bx, by) <= 0) {
return true;
}

// bottom
if (orient(ax, ay, bx, by, maxX, minY) * orient(ax, ay, bx, by, minX, minY) <= 0 &&
orient(maxX, minY, minX, minY, ax, ay) * orient(maxX, minY, minX, minY, bx, by) <= 0) {
return true;
}

// left
if (orient(ax, ay, bx, by, minX, minY) * orient(ax, ay, bx, by, minX, maxY) <= 0 &&
orient(minX, minY, minX, maxY, ax, ay) * orient(minX, minY, minX, maxY, bx, by) <= 0) {
return true;
}
return false;
}

private int numberOfCorners(float ax, float ay, float bx, float by, float cx, float cy) {
int containsCount = 0;
if (contains(ax, ay)) {
containsCount++;
}
if (contains(bx, by)) {
containsCount++;
}
if (contains(cx, cy)) {
containsCount++;
}
return containsCount;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof XYRectangle2D)) return false;
XYRectangle2D that = (XYRectangle2D) o;
return minX == that.minX &&
maxX == that.maxX &&
minY == that.minY &&
maxY == that.maxY;
}

@Override
public int hashCode() {
int result = Objects.hash(minX, maxX, minY, maxY);
return result;
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("XYRectangle(x=");
sb.append(decode(minX));
sb.append(minX);
sb.append(" TO ");
sb.append(decode(maxX));
sb.append(maxX);
sb.append(" y=");
sb.append(decode(minY));
sb.append(minY);
sb.append(" TO ");
sb.append(decode(maxY));
sb.append(maxY);
sb.append(")");
return sb.toString();
}

/** Builds a Rectangle2D from rectangle */
public static XYRectangle2D create(XYRectangle rectangle) {
return new XYRectangle2D((float)rectangle.minX, (float)rectangle.maxX, (float)rectangle.minY, (float)rectangle.maxY);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is right, float casts will round to the nearest float while we should actually round towards zero so that queries are accurate in the encoded space?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if the encoding works as you said. I test the following:

  public void testEncoding() {
   double val = ShapeTestUtil.nextDouble();
   double castVal = (double)(float) val;
   double qVal = XYEncodingUtils.decode(XYEncodingUtils.encode(val));
   assertEquals(castVal, qVal, 0.0);
  }

This seems to hold true.

}
}
Expand Up @@ -27,6 +27,7 @@
import org.apache.lucene.geo.XYPolygon2D;
import org.apache.lucene.geo.XYRectangle;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryUtils;

import static org.apache.lucene.geo.XYEncodingUtils.decode;
import static org.apache.lucene.geo.XYEncodingUtils.encode;
Expand Down Expand Up @@ -97,6 +98,34 @@ protected boolean rectCrossesDateline(Object rect) {
return false;
}

public void testBoxQueryEqualsAndHashcode() {
XYRectangle rectangle = ShapeTestUtil.nextBox();
QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
String fieldName = "foo";
Query q1 = newRectQuery(fieldName, queryRelation, rectangle.minX, rectangle.maxX, rectangle.minY, rectangle.maxY);
Query q2 = newRectQuery(fieldName, queryRelation, rectangle.minX, rectangle.maxX, rectangle.minY, rectangle.maxY);
QueryUtils.checkEqual(q1, q2);
//different field name
Query q3 = newRectQuery("bar", queryRelation, rectangle.minX, rectangle.maxX, rectangle.minY, rectangle.maxY);
QueryUtils.checkUnequal(q1, q3);
//different query relation
QueryRelation newQueryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
Query q4 = newRectQuery(fieldName, newQueryRelation, rectangle.minX, rectangle.maxX, rectangle.minY, rectangle.maxY);
if (queryRelation == newQueryRelation) {
QueryUtils.checkEqual(q1, q4);
} else {
QueryUtils.checkUnequal(q1, q4);
}
//different shape
XYRectangle newRectangle = ShapeTestUtil.nextBox();
Query q5 = newRectQuery(fieldName, queryRelation, newRectangle.minX, newRectangle.maxX, newRectangle.minY, newRectangle.maxY);
if (rectangle.equals(newRectangle)) {
QueryUtils.checkEqual(q1, q5);
} else {
QueryUtils.checkUnequal(q1, q5);
}
}

/** use {@link ShapeTestUtil#nextPolygon()} to create a random line; TODO: move to GeoTestUtil */
@Override
public XYLine nextLine() {
Expand All @@ -115,11 +144,67 @@ public static XYLine getNextLine() {
return new XYLine(x, y);
}

public void testLineQueryEqualsAndHashcode() {
XYLine line = nextLine();
QueryRelation queryRelation = RandomPicks.randomFrom(random(), POINT_LINE_RELATIONS);
String fieldName = "foo";
Query q1 = newLineQuery(fieldName, queryRelation, line);
Query q2 = newLineQuery(fieldName, queryRelation, line);
QueryUtils.checkEqual(q1, q2);
//different field name
Query q3 = newLineQuery("bar", queryRelation, line);
QueryUtils.checkUnequal(q1, q3);
//different query relation
QueryRelation newQueryRelation = RandomPicks.randomFrom(random(), POINT_LINE_RELATIONS);
Query q4 = newLineQuery(fieldName, newQueryRelation, line);
if (queryRelation == newQueryRelation) {
QueryUtils.checkEqual(q1, q4);
} else {
QueryUtils.checkUnequal(q1, q4);
}
//different shape
XYLine newLine = nextLine();
Query q5 = newLineQuery(fieldName, queryRelation, newLine);
if (line.equals(newLine)) {
QueryUtils.checkEqual(q1, q5);
} else {
QueryUtils.checkUnequal(q1, q5);
}
}

@Override
protected XYPolygon nextPolygon() {
return ShapeTestUtil.nextPolygon();
}

public void testPolygonQueryEqualsAndHashcode() {
XYPolygon polygon = nextPolygon();
QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
String fieldName = "foo";
Query q1 = newPolygonQuery(fieldName, queryRelation, polygon);
Query q2 = newPolygonQuery(fieldName, queryRelation, polygon);
QueryUtils.checkEqual(q1, q2);
//different field name
Query q3 = newPolygonQuery("bar", queryRelation, polygon);
QueryUtils.checkUnequal(q1, q3);
//different query relation
QueryRelation newQueryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
Query q4 = newPolygonQuery(fieldName, newQueryRelation, polygon);
if (queryRelation == newQueryRelation) {
QueryUtils.checkEqual(q1, q4);
} else {
QueryUtils.checkUnequal(q1, q4);
}
//different shape
XYPolygon newPolygon = nextPolygon();
Query q5 = newPolygonQuery(fieldName, queryRelation, newPolygon);
if (polygon.equals(newPolygon)) {
QueryUtils.checkEqual(q1, q5);
} else {
QueryUtils.checkUnequal(q1, q5);
}
}

@Override
protected Encoder getEncoder() {
return new Encoder() {
Expand Down
Expand Up @@ -81,15 +81,14 @@ public boolean testBBoxQuery(double minY, double maxY, double minX, double maxX,
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) {
ShapeField.DecodedTriangle decoded = encoder.encodeDecodeTriangle(line.getX(i), line.getY(i), true, line.getX(j), line.getY(j), true, line.getX(i), line.getY(i), true);
if (queryRelation == QueryRelation.WITHIN) {
if (rectangle2D.containsTriangle(decoded.aX, decoded.aY, decoded.bX, decoded.bY, decoded.cX, decoded.cY) == false) {
return false;
}
double[] qTriangle = encoder.quantizeTriangle(line.getX(i), line.getY(i), true, line.getX(j), line.getY(j), true, line.getX(i), line.getY(i), true);
Relation r = rectangle2D.relateTriangle((float)qTriangle[1], (float)qTriangle[0], (float)qTriangle[3], (float)qTriangle[2], (float)qTriangle[5], (float)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 (rectangle2D.intersectsTriangle(decoded.aX, decoded.aY, decoded.bX, decoded.bY, decoded.cX, decoded.cY) == true) {
return queryRelation == QueryRelation.INTERSECTS;
}
if (r != Relation.CELL_OUTSIDE_QUERY) return true;
}
}
return queryRelation != QueryRelation.INTERSECTS;
Expand Down
Expand Up @@ -72,17 +72,16 @@ public boolean testBBoxQuery(double minY, double maxY, double minX, double maxX,
XYRectangle2D rectangle2D = XYRectangle2D.create(new XYRectangle(minX, maxX, minY, maxY));
List<Tessellator.Triangle> tessellation = Tessellator.tessellate(p);
for (Tessellator.Triangle t : tessellation) {
ShapeField.DecodedTriangle decoded = encoder.encodeDecodeTriangle(t.getX(0), t.getY(0), t.isEdgefromPolygon(0),
t.getX(1), t.getY(1), t.isEdgefromPolygon(1),
t.getX(2), t.getY(2), t.isEdgefromPolygon(2));
if (queryRelation == QueryRelation.WITHIN) {
if (rectangle2D.containsTriangle(decoded.aX, decoded.aY, decoded.bX, decoded.bY, decoded.cX, decoded.cY) == false) {
return false;
}
double[] qTriangle = encoder.quantizeTriangle(t.getX(0), t.getY(0), t.isEdgefromPolygon(0),
t.getX(1), t.getY(1), t.isEdgefromPolygon(1),
t.getX(2), t.getY(2), t.isEdgefromPolygon(2));
Relation r = rectangle2D.relateTriangle((float)qTriangle[1], (float)qTriangle[0], (float)qTriangle[3], (float)qTriangle[2], (float)qTriangle[5], (float)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 (rectangle2D.intersectsTriangle(decoded.aX, decoded.aY, decoded.bX, decoded.bY, decoded.cX, decoded.cY) == true) {
return queryRelation == QueryRelation.INTERSECTS;
}
if (r != Relation.CELL_OUTSIDE_QUERY) return true;
}
}
return queryRelation != QueryRelation.INTERSECTS;
Expand Down