diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/Component2D.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/Component2D.java new file mode 100644 index 000000000000..49de20d8dd74 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/Component2D.java @@ -0,0 +1,70 @@ +/* + * 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.component2D; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.index.PointValues; + +import static org.apache.lucene.geo.GeoUtils.orient; + +/** + * 2D Geometry object that supports spatial relationships with bounding boxes, + * triangles and points. + * + * @lucene.internal + */ +public interface Component2D { + /** relates this component2D with a point **/ + boolean contains(int x, int y); + + /** relates this component2D with a bounding box **/ + PointValues.Relation relate(int minX, int maxX, int minY, int maxY); + + /** relates this component2D with a triangle **/ + PointValues.Relation relateTriangle(int minX, int maxX, int minY, int maxY, int aX, int aY, int bX, int bY, int cX, int cY); + + /** relates this component2D with a triangle **/ + default PointValues.Relation relateTriangle(int aX, int aY, int bX, int bY, int cX, int cY) { + int minY = StrictMath.min(StrictMath.min(aY, bY), cY); + int minX = StrictMath.min(StrictMath.min(aX, bX), cX); + int maxY = StrictMath.max(StrictMath.max(aY, bY), cY); + int maxX = StrictMath.max(StrictMath.max(aX, bX), cX); + return relateTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY); + } + + /** bounding box for this component2D **/ + RectangleComponent2D getBoundingBox(); + + /** + * Compute whether the given x, y point is in a triangle; uses the winding order method */ + static boolean pointInTriangle(double minX, double maxX, double minY, double maxY, double x, double y, double aX, double aY, double bX, double bY, double cX, double cY) { + //check the bounding box because if the triangle is degenerated, e.g points and lines, we need to filter out + //coplanar points that are not part of the triangle. + if (x >= minX && x <= maxX && y >= minY && y <= maxY) { + int a = orient(x, y, aX, aY, bX, bY); + int b = orient(x, y, bX, bY, cX, cY); + if (a == 0 || b == 0 || a < 0 == b < 0) { + int c = orient(x, y, cX, cY, aX, aY); + return c == 0 || (c < 0 == (b < 0 || a < 0)); + } + return false; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/Component2DTree.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/Component2DTree.java new file mode 100644 index 000000000000..3aad5813c577 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/Component2DTree.java @@ -0,0 +1,215 @@ +/* + * 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.component2D; + +import java.util.Comparator; +import java.util.Objects; + +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.ArrayUtil; + +/** + * 2D geometry collection implementation represented as a r-tree of {@link Component2D}. + * + * @lucene.internal + */ +class Component2DTree implements Component2D { + /** root node of edge tree */ + private final Component2D component; + /** box of this component2D and its children or null if there is no children */ + private RectangleComponent2D box; + // child components, or null + private Component2DTree left; + private Component2DTree right; + + private Component2DTree(Component2D component) { + this.component = component; + this.box = null; + } + + @Override + public boolean contains(int x, int y) { + if (box == null || box.contains(x, y)) { + if (component.contains(x, y)) { + return true; + } + if (left != null && left.contains(x, y)) { + return true; + } + if (right != null && right.contains(x, y)) { + return true; + } + } + return false; + } + + @Override + public Relation relate(int minX, int maxX, int minY, int maxY) { + if (box == null || box.disjoint(minX, maxX, minY, maxY) == false) { + Relation relation = component.relate(minX, maxX, minY, maxY); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + + if (left != null) { + relation = left.relate(minX, maxX, minY, maxY); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + if (right != null) { + relation = right.relate(minX, maxX, minY, maxY); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + } + return Relation.CELL_OUTSIDE_QUERY; + } + + @Override + public RectangleComponent2D getBoundingBox() { + return box == null ? component.getBoundingBox() : box; + } + + /** Returns relation to the provided triangle */ + @Override + public Relation relateTriangle(int minX, int maxX, int minY, int maxY, int aX, int aY, int bX, int bY, int cX, int cY) { + if (box == null || box.disjoint(minX, maxX, minY, maxY) == false) { + Relation relation = component.relateTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + if (left != null) { + relation = left.relateTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + if (right != null) { + relation = right.relateTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + } + return Relation.CELL_OUTSIDE_QUERY; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Component2DTree componentTree = (Component2DTree) o; + return Objects.equals(component, componentTree.component) && + Objects.equals(left, componentTree.left) && + Objects.equals(right, componentTree.right); + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Objects.hashCode(component); + result = (int) (temp ^ (temp >>> 32)); + temp = Objects.hashCode(left); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Objects.hashCode(right); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "PolygonComponent2D{" + + "compoment=" + component + " left=" + Objects.toString(left) + " right=" + Objects.toString(right) + + '}'; + } + + /** Creates tree from sorted components (with full bounding box for children) */ + protected static Component2DTree createTree(Component2D components[], int low, int high, boolean splitX) { + if (low > high) { + return null; + } + final int mid = (low + high) >>> 1; + if (low < high) { + Comparator comparator; + if (splitX) { + comparator = (left, right) -> { + int ret = Integer.compare(left.getBoundingBox().minX, right.getBoundingBox().minX); + if (ret == 0) { + ret = Integer.compare(left.getBoundingBox().maxX, right.getBoundingBox().maxX); + } + return ret; + }; + } else { + comparator = (left, right) -> { + int ret = Integer.compare(left.getBoundingBox().minY, right.getBoundingBox().minY); + if (ret == 0) { + ret = Integer.compare(left.getBoundingBox().maxY, right.getBoundingBox().maxY); + } + return ret; + }; + } + ArrayUtil.select(components, low, high + 1, mid, comparator); + } else { + return new Component2DTree(components[mid]); + } + // add midpoint + Component2DTree newNode = new Component2DTree(components[mid]); + // add children + newNode.left = createTree(components, low, mid - 1, !splitX); + newNode.right = createTree(components, mid + 1, high, !splitX); + if (newNode.left != null || newNode.right != null) { + // pull up bounding box values to this node + int minX = newNode.component.getBoundingBox().minX; + int maxX = newNode.component.getBoundingBox().maxX; + int minY = newNode.component.getBoundingBox().minY; + int maxY = newNode.component.getBoundingBox().maxY; + if (newNode.left != null) { + maxX = Math.max(maxX, newNode.left.getBoundingBox().maxX); + maxY = Math.max(maxY, newNode.left.getBoundingBox().maxY); + minX = splitX == true ? newNode.left.getBoundingBox().minX : Math.min(minX, newNode.left.getBoundingBox().minX); + minY = splitX == false ? newNode.left.getBoundingBox().minY : Math.min(minY, newNode.left.getBoundingBox().minY); + } + if (newNode.right != null) { + maxX = Math.max(maxX, newNode.right.getBoundingBox().maxX); + maxY = Math.max(maxY, newNode.right.getBoundingBox().maxY); + minX = splitX == true ? minX : Math.min(minX, newNode.right.getBoundingBox().minX); + minY = splitX == false ? minY : Math.min(minY, newNode.right.getBoundingBox().minY); + } + newNode.box = RectangleComponent2D.createComponent(minX, maxX, minY, maxY); + } + assert newNode.left == null || (newNode.getBoundingBox().minX <= newNode.left.getBoundingBox().minX && + newNode.getBoundingBox().maxX >= newNode.left.getBoundingBox().maxX && + newNode.getBoundingBox(). minY <= newNode.left.getBoundingBox().minY && + newNode.getBoundingBox().maxY >= newNode.left.getBoundingBox().maxY); + assert newNode.right == null || (newNode.getBoundingBox().minX <= newNode.right.getBoundingBox().minX && + newNode.getBoundingBox().maxX >= newNode.right.getBoundingBox().maxX && + newNode.getBoundingBox().minY <= newNode.right.getBoundingBox().minY && + newNode.getBoundingBox().maxY >= newNode.right.getBoundingBox().maxY); + return newNode; + } + + /** Builds a Component2D from multiple components in a tree structure */ + protected static Component2D create(Component2D... components) { + if (components.length == 1) { + return components[0]; + } + return Component2DTree.createTree(components, 0, components.length - 1, true); + } +} \ No newline at end of file diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/EdgeTree.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/EdgeTree.java new file mode 100644 index 000000000000..757a3d35a8ee --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/EdgeTree.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.component2D; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.lucene.geo.GeoUtils; + +import static org.apache.lucene.geo.GeoUtils.lineCrossesLine; +import static org.apache.lucene.geo.GeoUtils.lineCrossesLineWithBoundary; +import static org.apache.lucene.geo.GeoUtils.orient; + +/** + * 2D line/polygon geometry implementation represented as a balanced interval tree of edges. + *

+ * Construction takes {@code O(n log n)} time for sorting and tree construction. + * Crosses methods are {@code O(n)}, but for most + * practical lines and polygons are much faster than brute force. + * @lucene.internal + */ +class EdgeTree { + final double x1, x2; + final double y1, y2; + + /** min of this edge */ + final double low; + /** max latitude of this edge or any children */ + double max; + + /** left child edge, or null */ + EdgeTree left; + /** right child edge, or null */ + EdgeTree right; + + protected EdgeTree(final double x1, final double y1, final double x2, final double y2, final double low, final double max) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.low = low; + this.max = max; + } + + /** + * Returns true if the point crosses this edge subtree an odd number of times + *

+ * See + * https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html for more information. + */ + // ported to java from https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html + // original code under the BSD license (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html#License%20to%20Use) + // + // Copyright (c) 1970-2003, Wm. Randolph Franklin + // + // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + // documentation files (the "Software"), to deal in the Software without restriction, including without limitation + // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + // to permit persons to whom the Software is furnished to do so, subject to the following conditions: + // + // 1. Redistributions of source code must retain the above copyright + // notice, this list of conditions and the following disclaimers. + // 2. Redistributions in binary form must reproduce the above copyright + // notice in the documentation and/or other materials provided with + // the distribution. + // 3. The name of W. Randolph Franklin may not be used to endorse or + // promote products derived from this Software without specific + // prior written permission. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + // IN THE SOFTWARE. + public boolean contains(double x, double y, AtomicBoolean isOnEdge) { + // crossings algorithm is an odd-even algorithm, so we descend the tree xor'ing results along our path + boolean res = false; + if (isOnEdge.get() == false && y <= max) { + if (y == y1 && y == y2 || + (y <= y1 && y >= y2) != (y >= y1 && y <= y2)) { + if ((x == x1 && x == x2) || + ((x <= x1 && x >= x2) != (x >= x1 && x <= x2) && + GeoUtils.orient(x1, y1, x2, y2, x, y) == 0)) { + // if its on the boundary return true + isOnEdge.set(true); + return true; + } else if (y1 > y != y2 > y) { + res = x < (x2 - x1) * (y - y1) / (y2 - y1) + x1; + } + } + if (left != null) { + res ^= left.contains(x, y, isOnEdge); + } + + if (right != null && y >= low) { + res ^= right.contains(x, y, isOnEdge); + } + } + return isOnEdge.get() || res; + } + + boolean crossesTriangle(double minX, double maxX, double minY, double maxY, double aX, double aY, double bX, double bY, double cX, double cY) { + if (minY <= max) { + double dX = x1; + double dY = y1; + double eX = x2; + double eY = y2; + + // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all + // if not, don't waste our time trying more complicated stuff + boolean outside = (dY < minY && eY < minY) || + (dY > maxY && eY > maxY) || + (dX < minX && eX < minX) || + (dX > maxX && eX > maxX); + + if (outside == false) { + // does triangle's edges intersect polyline? + if (lineCrossesLine(aX, aY, bX, bY, dX, dY, eX, eY) || + lineCrossesLine(bX, bY, cX, cY, dX, dY, eX, eY) || + lineCrossesLine(cX, cY, aX, aY, dX, dY, eX, eY)) { + return true; + } + } + + if (left != null && left.crossesTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY)) { + return true; + } + + if (right != null && maxY >= low && right.crossesTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY)) { + return true; + } + } + return false; + } + + /** returns true if this {@link RectangleComponent2D} contains the encoded lat lon point */ + private static boolean containsPoint(final double x, final double y, final double minX, final double maxX, final double minY, final double maxY) { + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + /** Returns true if the box crosses any edge in this edge subtree */ + boolean crossesBox(double minX, double maxX, double minY, double maxY, boolean includeBoundary) { + // we just have to cross one edge to answer the question, so we descend the tree and return when we do. + if (minY <= max) { + // we compute line intersections of every polygon edge with every box line. + // if we find one, return true. + // for each box line (AB): + // for each poly line (CD): + // intersects = orient(C,D,A) * orient(C,D,B) <= 0 && orient(A,B,C) * orient(A,B,D) <= 0 + double cX = x1; + double cY = y1; + double dX = x2; + double dY = y2; + + // optimization: see if either end of the line segment is contained by the rectangle + if (containsPoint(cX, cY, minX, maxX, minY, maxY) || + containsPoint(dX, dY, minX, maxX, minY, maxY)) { + return true; + } + + // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all + // if not, don't waste our time trying more complicated stuff + boolean outside = (cX < minX && dX < minX) || + (cX > maxX && dX > maxX) || + (cY < minY && dY < minY) || + (cY > maxY && dY > maxY); + + // does rectangle's edges intersect polyline? + if (outside == false) { + if (includeBoundary == true && + lineCrossesLineWithBoundary(cX, cY, dX, dY, minX, minY, maxX, minY) || + lineCrossesLineWithBoundary(cX, cY, dX, dY, maxX, minY, maxX, maxY) || + lineCrossesLineWithBoundary(cX, cY, dX, dY, maxX, maxY, minX, maxY) || + lineCrossesLineWithBoundary(cX, cY, dX, dY, minX, maxY, minX, minY)) { + // include boundaries: ensures box edges that terminate on the polygon are included + return true; + } else if (lineCrossesLine(cX, cY, dX, dY, minX, minY, maxX, minY) || + lineCrossesLine(cX, cY, dX, dY, maxX, minY, maxX, maxY) || + lineCrossesLine(cX, cY, dX, dY, maxX, maxY, minX, maxY) || + lineCrossesLine(cX, cY, dX, dY, minX, maxY, minX, minY)) { + return true; + } + } + + if (left != null && left.crossesBox(minX, maxX, minY, maxY, includeBoundary)) { + return true; + } + + if (right != null && maxY >= low && right.crossesBox(minX, maxX, minY, maxY, includeBoundary)) { + return true; + } + } + return false; + } + + /** Returns true if the line crosses any edge in this edge subtree */ + boolean crossesLine(double a2X, double a2Y, double b2X, double b2Y) { + double minY = StrictMath.min(a2Y, b2Y); + double maxY = StrictMath.max(a2Y, b2Y); + if (minY <= max) { + double a1X = x1; + double a1Y = y1; + double b1X = x2; + double b1Y = y2; + + double minX = StrictMath.min(a2X, b2X); + double maxX = StrictMath.max(a2X, b2X); + + boolean outside = (a1Y < minY && b1Y < minY) || + (a1Y > maxY && b1Y > maxY) || + (a1X < minX && b1X < minX) || + (a1X > maxX && b1X > maxX); + if (outside == false && lineCrossesLineWithBoundary(a1X, a1Y, b1X, b1Y, a2X, a2Y, b2X, b2Y)) { + return true; + } + + if (left != null && left.crossesLine(a2X, a2Y, b2X, b2Y)) { + return true; + } + if (right != null && maxY >= low && right.crossesLine(a2X, a2Y, b2X, b2Y)) { + return true; + } + } + return false; + } + + /** returns true if the provided x, y point lies on any of the edges */ + boolean pointInEdge(double x, double y) { + if (y <= max) { + double minY = StrictMath.min(y1, y2); + double maxY = StrictMath.max(y1, y2); + double minX = StrictMath.min(x1, x2); + double maxX = StrictMath.max(x1, x2); + if (containsPoint(x, y, minX, maxX, minY, maxY) && + orient(x1, y1, x2, y2, x, y) == 0) { + return true; + } + if (left != null && left.pointInEdge(x, y)) { + return true; + } + if (right != null && maxY >= low && right.pointInEdge(x, y)) { + return true; + } + } + return false; + } + + /** + * Creates an edge interval tree from a set of geometry vertices. + * @return root node of the tree. + */ + public static EdgeTree createTree(double[] Xs, double[] Ys) { + EdgeTree edges[] = new EdgeTree[Ys.length - 1]; + for (int i = 1; i < Ys.length; i++) { + double x1 = Xs[i-1]; + double y1 = Ys[i-1]; + double x2 = Xs[i]; + double y2 = Ys[i]; + + edges[i - 1] = new EdgeTree(x1, y1, x2, y2, Math.min(y1, y2), Math.max(y1, y2)); + } + // sort the edges then build a balanced tree from them + Arrays.sort(edges, (left, right) -> { + int ret = Double.compare(left.low, right.low); + if (ret == 0) { + ret = Double.compare(left.max, right.max); + } + return ret; + }); + return createTree(edges, 0, edges.length - 1); + } + + /** Creates tree from sorted edges (with range low and high inclusive) */ + private static EdgeTree createTree(EdgeTree edges[], int low, int high) { + if (low > high) { + return null; + } + // add midpoint + int mid = (low + high) >>> 1; + EdgeTree newNode = edges[mid]; + // add children + newNode.left = createTree(edges, low, mid - 1); + newNode.right = createTree(edges, mid + 1, high); + // pull up max values to this node + if (newNode.left != null) { + newNode.max = Math.max(newNode.max, newNode.left.max); + } + if (newNode.right != null) { + newNode.max = Math.max(newNode.max, newNode.right.max); + } + return newNode; + } +} \ No newline at end of file diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/LatLonComponent2DFactory.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/LatLonComponent2DFactory.java new file mode 100644 index 000000000000..f47f644d8286 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/LatLonComponent2DFactory.java @@ -0,0 +1,178 @@ +/* + * 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.component2D; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.lucene.document.LatLonShape; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.Rectangle; + +/** Factory methods for creating {@link Component2D} from LatLon objects. + * + * @lucen.internal + */ + +public class LatLonComponent2DFactory { + + /** Builds a Component2D from multi-point on the form [Lat, Lon] */ + public static Component2D create(double[]... points) { + if (points.length == 1) { + GeoUtils.checkLatitude(points[0][0]); + GeoUtils.checkLongitude(points[0][1]); + return PointComponent2D.createComponent(GeoEncodingUtils.encodeLongitude(points[0][1]), GeoEncodingUtils.encodeLatitude(points[0][0])); + } + Component2D components[] = new Component2D[points.length]; + for (int i = 0; i < components.length; i++) { + GeoUtils.checkLatitude(points[i][0]); + GeoUtils.checkLongitude(points[i][1]); + components[i] = PointComponent2D.createComponent(GeoEncodingUtils.encodeLongitude(points[i][1]), GeoEncodingUtils.encodeLatitude(points[i][0])); + } + return Component2DTree.create(components); + } + + private static final int NEGATIVE_DATELINE = GeoEncodingUtils.encodeLongitude(-180); + private static final int POSITIVE_DATELINE = GeoEncodingUtils.encodeLongitude(180); + + /** Builds a Component2D from multi-rectangle */ + public static Component2D create(Rectangle... rectangles) { + if (rectangles.length == 1 && rectangles[0].crossesDateline() == false) { + int minX = GeoEncodingUtils.encodeLongitude(rectangles[0].minLon); + int maxX = GeoEncodingUtils.encodeLongitude(rectangles[0].maxLon); + int minY = GeoEncodingUtils.encodeLatitude(rectangles[0].minLat); + int maxY = GeoEncodingUtils.encodeLatitude(rectangles[0].maxLat); + return RectangleComponent2D.createComponent(minX, maxX, minY, maxY); + } + List components = new ArrayList<>(); + for (Rectangle rectangle: rectangles) { + int minX = GeoEncodingUtils.encodeLongitude(rectangle.minLon); + int maxX = GeoEncodingUtils.encodeLongitude(rectangle.maxLon); + int minY = GeoEncodingUtils.encodeLatitude(rectangle.minLat); + int maxY = GeoEncodingUtils.encodeLatitude(rectangle.maxLat); + if (rectangle.crossesDateline()) { + components.add(RectangleComponent2D.createComponent(minX, POSITIVE_DATELINE, minY, maxY)); + components.add(RectangleComponent2D.createComponent(NEGATIVE_DATELINE, maxX, minY, maxY)); + } else { + components.add(RectangleComponent2D.createComponent(minX, maxX, minY, maxY)); + } + } + return Component2DTree.create(components.toArray(new Component2D[components.size()])); + } + + /** Builds a Component2D from polygon */ + private static Component2D createComponent(Polygon polygon) { + Polygon gonHoles[] = polygon.getHoles(); + Component2D holes = null; + if (gonHoles.length > 0) { + holes = create(gonHoles); + } + RectangleComponent2D box = RectangleComponent2D.createComponent(GeoEncodingUtils.encodeLongitude(polygon.minLon), + GeoEncodingUtils.encodeLongitude(polygon.maxLon), + GeoEncodingUtils.encodeLatitude(polygon.minLat), + GeoEncodingUtils.encodeLatitude(polygon.maxLat)); + return new PolygonComponent2D(quatizeLongs(polygon.getPolyLons()), quantizeLats(polygon.getPolyLats()), box, holes, LatLonShape.DECODER); + } + + /** Builds a Component2D tree from multipolygon */ + public static Component2D create(Polygon... polygons) { + if (polygons.length == 1) { + return createComponent(polygons[0]); + } + Component2D components[] = new Component2D[polygons.length]; + for (int i = 0; i < components.length; i++) { + components[i] = createComponent(polygons[i]); + } + return Component2DTree.create(components); + } + + /** Builds a Component2D from line */ + private static Component2D createComponent(Line line) { + RectangleComponent2D box = RectangleComponent2D.createComponent(GeoEncodingUtils.encodeLongitude(line.minLon), + GeoEncodingUtils.encodeLongitude(line.maxLon), + GeoEncodingUtils.encodeLatitude(line.minLat), + GeoEncodingUtils.encodeLatitude(line.maxLat)); + return new LineComponent2D(quatizeLongs(line.getLons()), quantizeLats(line.getLats()), box, LatLonShape.DECODER); + } + + /** Builds a Component2D tree from multiline */ + public static Component2D create(Line... lines) { + if (lines.length == 1) { + return createComponent(lines[0]); + } + Component2D components[] = new Component2D[lines.length]; + for (int i = 0; i < components.length; i++) { + components[i] = createComponent(lines[i]); + } + return Component2DTree.create(components); + } + + /** Builds a Component2D from polygon */ + private static Component2D createComponent(Object shape) { + if (shape instanceof double[]) { + return create((double[]) shape); + } else if (shape instanceof Polygon) { + return create((Polygon) shape); + } else if (shape instanceof Line) { + return create((Line) shape); + } else if (shape instanceof Rectangle) { + return create((Rectangle) shape); + } else { + throw new IllegalArgumentException("Unknown shape type: " + shape.getClass()); + } + } + + /** Builds a Component2D from an array of shape descriptors. Current descriptors supported are: + * {@link Polygon}, {@link Line}, {@link Rectangle} and double[] ([Lat, Lon] point). + * */ + public static Component2D create(Object... shapes) { + if (shapes.length == 1) { + return createComponent(shapes[0]); + } + Component2D[] components = new Component2D[shapes.length]; + for (int i = 0; i < shapes.length; i ++) { + components[i] = createComponent(shapes[i]); + } + return Component2DTree.createTree(components, 0, components.length - 1, true); + } + + private static double[] quatizeLongs(double[] longs) { + double[] encoded = new double[longs.length]; + for (int i = 0; i < longs.length; i++) { + encoded[i] = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(longs[i])); + } + return encoded; + } + + private static double[] quantizeLats(double[] lats) { + double[] encoded = new double[lats.length]; + for (int i = 0; i < lats.length; i++) { + encoded[i] = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(lats[i])); + } + return encoded; + } + + /** Builds a Component predicate for fast computation of point in component. The component must + * be created with one of the methods of this factory */ + public static LatLonComponent2DPredicate createComponentPredicate(Component2D component) { + return LatLonComponent2DPredicate.createComponentPredicate(component); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/LatLonComponent2DPredicate.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/LatLonComponent2DPredicate.java new file mode 100644 index 000000000000..459fd513db21 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/LatLonComponent2DPredicate.java @@ -0,0 +1,162 @@ +/* + * 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.component2D; + +import java.util.function.Function; + +import org.apache.lucene.index.PointValues; + +/** + * + * A component2D predicate for fast computation of point in component computation. + * + * @lucene.internal + */ +public class LatLonComponent2DPredicate { + + private final Component2D component; + + static final int ARITY = 64; + + final int yShift, xShift; + final int yBase, xBase; + final int maxYDelta, maxXDelta; + final byte[] relations; + + private LatLonComponent2DPredicate( + int yShift, int xShift, + int yBase, int xBase, + int maxYDelta, int maxXDelta, + byte[] relations, + Component2D component) { + if (yShift < 1 || yShift > 31) { + throw new IllegalArgumentException(); + } + if (xShift < 1 || xShift > 31) { + throw new IllegalArgumentException(); + } + this.yShift = yShift; + this.xShift = xShift; + this.yBase = yBase; + this.xBase = xBase; + this.maxYDelta = maxYDelta; + this.maxXDelta = maxXDelta; + this.relations = relations; + this.component = component; + } + + /** Check whether the given point is within the considered component. + * NOTE: this operates directly on the encoded representation of points. */ + public boolean test(int x, int y) { + final int y2 = ((y - Integer.MIN_VALUE) >>> yShift); + if (y2 < yBase || y2 >= yBase + maxYDelta) { + // not sure about this but it fails in some cases for point components + return false; + } + int x2 = ((x - Integer.MIN_VALUE) >>> xShift); + if (x2 < xBase) { // wrap + x2 += 1 << (32 - xShift); + } + assert Integer.toUnsignedLong(x2) >= xBase; + assert x2 - xBase >= 0; + if (x2 - xBase >= maxXDelta) { + return false; + } + + final int relation = relations[(y2 - yBase) * maxXDelta + (x2 - xBase)]; + if (relation == PointValues.Relation.CELL_CROSSES_QUERY.ordinal()) { + return component.contains(x, y); + } else { + return relation == PointValues.Relation.CELL_INSIDE_QUERY.ordinal(); + } + } + + private static LatLonComponent2DPredicate createSubBoxes(RectangleComponent2D boundingBox, Function boxToRelation, Component2D component) { + final int minY = boundingBox.minY; + final int maxY = boundingBox.maxY; + final int minX = boundingBox.minX; + final int maxX = boundingBox.maxX; + + final int yShift, xShift; + final int yBase, xBase; + final int maxYDelta, maxXDelta; + { + long minY2 = (long) minY - Integer.MIN_VALUE; + long maxY2 = (long) maxY - Integer.MIN_VALUE; + yShift = computeShift(minY2, maxY2); + yBase = (int) (minY2 >>> yShift); + maxYDelta = (int) (maxY2 >>> yShift) - yBase + 1; + assert maxYDelta > 0; + } + { + long minX2 = (long) minX - Integer.MIN_VALUE; + long maxX2 = (long) maxX - Integer.MIN_VALUE; + xShift = computeShift(minX2, maxX2); + xBase = (int) (minX2 >>> xShift); + maxXDelta = (int) (maxX2 >>> xShift) - xBase + 1; + assert maxXDelta > 0; + } + + final byte[] relations = new byte[maxYDelta * maxXDelta]; + for (int i = 0; i < maxYDelta; ++i) { + for (int j = 0; j < maxXDelta; ++j) { + final int boxMinY = ((yBase + i) << yShift) + Integer.MIN_VALUE; + final int boxMinX = ((xBase + j) << xShift) + Integer.MIN_VALUE; + final int boxMaxY = boxMinY + (1 << yShift) - 1; + final int boxMaxX = boxMinX + (1 << xShift) - 1; + + //System.out.println(boxMinX + " " + boxMaxX + " " + boxMinY + " " + boxMaxY); + relations[i * maxXDelta + j] = (byte) boxToRelation.apply(RectangleComponent2D.createComponent( + boxMinX, boxMaxX, + boxMinY, boxMaxY)).ordinal(); + } + } + + return new LatLonComponent2DPredicate( + yShift, xShift, + yBase, xBase, + maxYDelta, maxXDelta, + relations, component); + } + + /** Compute the minimum shift value so that + * {@code (b>>>shift)-(a>>>shift)} is less that {@code ARITY}. */ + private static int computeShift(long a, long b) { + assert a <= b; + // We enforce a shift of at least 1 so that when we work with unsigned ints + // by doing (lat - MIN_VALUE), the result of the shift (lat - MIN_VALUE) >>> shift + // can be used for comparisons without particular care: the sign bit has + // been cleared so comparisons work the same for signed and unsigned ints + for (int shift = 1; ; ++shift) { + final long delta = (b >>> shift) - (a >>> shift); + if (delta >= 0 && delta < LatLonComponent2DPredicate.ARITY) { + return shift; + } + } + } + + /** Create a predicate that checks whether points are within a component2D. + * @lucene.internal */ + static LatLonComponent2DPredicate createComponentPredicate(Component2D component) { + final RectangleComponent2D boundingBox = component.getBoundingBox(); + final Function boxToRelation = box -> component.relate( + box.minX, box.maxX, box.minY, box.maxY); + return createSubBoxes(boundingBox, boxToRelation, component); + } + +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/LineComponent2D.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/LineComponent2D.java new file mode 100644 index 000000000000..6130379e79e5 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/LineComponent2D.java @@ -0,0 +1,133 @@ +/* + * 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.component2D; + +import java.util.Arrays; + +import org.apache.lucene.document.ShapeField; +import org.apache.lucene.index.PointValues; + +import static org.apache.lucene.geo.GeoUtils.orient; + +/** Represents a 2D line. + * + * @lucene.internal + * */ +class LineComponent2D implements Component2D { + + /** X values, used for equality and hashcode */ + private final double[] Xs; + /** Y values, used for equality and hashcode */ + private final double[] Ys; + /** edge tree representing the line */ + private final EdgeTree tree; + /** bounding box of the line */ + private final RectangleComponent2D box; + + private final ShapeField.Decoder decoder; + + protected LineComponent2D(double[] Xs, double[] Ys, RectangleComponent2D box, ShapeField.Decoder decoder) { + this.Xs = Xs; + this.Ys = Ys; + this.tree = EdgeTree.createTree(Xs, Ys); + this.box = box; + this.decoder = decoder; + } + + @Override + public boolean contains(int x, int y) { + if (box.contains(x, y)) { + return tree.pointInEdge(decoder.decodeX(x), decoder.decodeY(y)); + } + return false; + } + + @Override + public PointValues.Relation relate(int minX, int maxX, int minY, int maxY) { + if (box.disjoint(minX, maxX, minY, maxY)) { + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + if (box.within(minX, maxX, minY, maxY) || tree.crossesBox(decoder.decodeX(minX), decoder.decodeX(maxX), decoder.decodeY(minY), decoder.decodeY(maxY), true)) { + return PointValues.Relation.CELL_CROSSES_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + + @Override + public PointValues.Relation relateTriangle(int minX, int maxX, int minY, int maxY, int aX, int aY, int bX, int bY, int cX, int cY) { + if (box.disjoint(minX, maxX, minY, maxY)) { + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + if (aX == bX && bX == cX && aY == bY && bY == cY) { + // indexed "triangle" is a point: check if point lies on any line segment + if (contains(aX, aY)) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } else if ((aX == cX && aY == cY) || (bX == cX && bY == cY)) { + // indexed "triangle" is a line: + if (tree.crossesLine(decoder.decodeX(aX), decoder.decodeY(aY), decoder.decodeX(bX), decoder.decodeY(bY))) { + return PointValues.Relation.CELL_CROSSES_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } else { + if (crossesTriangle(decoder.decodeX(minX), decoder.decodeX(maxX), decoder.decodeY(minY), decoder.decodeY(maxY), + decoder.decodeX(aX), decoder.decodeY(aY), decoder.decodeX(bX), decoder.decodeY(bY), decoder.decodeX(cX), decoder.decodeY(cY))) { + // indexed "triangle" is a triangle: + return PointValues.Relation.CELL_CROSSES_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + } + + private boolean crossesTriangle(double minX, double maxX, double minY, double maxY, double aX, double aY, double bX, double bY, double cX, double cY) { + return Component2D.pointInTriangle(minX, maxX, minY, maxY, tree.x1, tree.y1, aX, aY, bX, bY, cX, cY) == true || + tree.crossesTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY); + } + + @Override + public RectangleComponent2D getBoundingBox() { + return box; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LineComponent2D lineComponent = (LineComponent2D) o; + return Arrays.equals(Xs, lineComponent.Xs) && Arrays.equals(Ys, lineComponent.Ys); + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Arrays.hashCode(Xs); + result = (int) (temp ^ (temp >>> 32)); + temp = Arrays.hashCode(Ys); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "LineComponent2D{" + + "Xs=" + Arrays.toString(Xs) + ", Ys=" + Arrays.toString(Ys) + + '}'; + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/PointComponent2D.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/PointComponent2D.java new file mode 100644 index 000000000000..59f741c264b3 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/PointComponent2D.java @@ -0,0 +1,101 @@ +/* + * 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.component2D; + +import org.apache.lucene.index.PointValues; + +/** Represents a 2D point. + * + * @lucene.internal + * */ +class PointComponent2D implements Component2D { + + /** X value */ + final int x; + /** Y value */ + final int y; + /** bounding box of the point */ + final RectangleComponent2D box; + + private PointComponent2D(int x, int y) { + this.x = x; + this.y = y; + box = RectangleComponent2D.createComponent(x, x, y, y); + } + + @Override + public boolean contains(int x, int y) { + return this.x == x && this.y == y; + } + + @Override + public PointValues.Relation relate(int minX, int maxX, int minY, int maxY) { + if (box.within(minX, maxX, minY, maxY)) { + if (minY == maxY && y == minY && minX == maxX && maxX == x) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } + return PointValues.Relation.CELL_CROSSES_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + + @Override + public PointValues.Relation relateTriangle(int minX, int maxX, int minY, int maxY, int aX, int aY, int bX, int bY, int cX, int cY) { + if (aX == bX && bX == cX && aY == bY && bY == cY) { + // indexed "triangle" is a point: shortcut by checking contains + return contains(minX, minY) ? PointValues.Relation.CELL_INSIDE_QUERY : PointValues.Relation.CELL_OUTSIDE_QUERY; + } + if (RectangleComponent2D.containsPoint(x, y, minX, maxX, minY, maxY) && + Component2D.pointInTriangle(minX, maxX, minY, maxY, x, y, aX, aY, bX, bY, cX, cY)) { + return PointValues.Relation.CELL_CROSSES_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + + @Override + public RectangleComponent2D getBoundingBox() { + return box; + } + + @Override + public int hashCode() { + int result = Integer.hashCode(x); + result = 31 * result + Integer.hashCode(y); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PointComponent2D pointComponent = (PointComponent2D) o; + return x == pointComponent.x && y == pointComponent.y; + } + + @Override + public String toString() { + return "PointComponent2D{" + + "Point=[" + x + "," + y +"]" + + '}'; + } + + /** Builds a Component2D from a x and y */ + protected static Component2D createComponent(int x, int y) { + return new PointComponent2D(x, y); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/PolygonComponent2D.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/PolygonComponent2D.java new file mode 100644 index 000000000000..7c4ebc40d644 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/PolygonComponent2D.java @@ -0,0 +1,258 @@ +/* + * 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.component2D; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.lucene.document.ShapeField; +import org.apache.lucene.index.PointValues.Relation; + +/** + * Represents a 2D polygon + * + * @lucene.internal + */ +final class PolygonComponent2D implements Component2D { + /** X values, used for equality and hashcode */ + private final double[] Xs; + /** Y values, used for equality and hashcode */ + private final double[] Ys; + /** edge tree representing the polygon */ + private final EdgeTree tree; + /** bounding box of the polygon */ + private final RectangleComponent2D box; + /** Holes component2D or null */ + private final Component2D holes; + /** keeps track if points lies on polygon boundary */ + private final AtomicBoolean containsBoundary = new AtomicBoolean(false); + + private final ShapeField.Decoder decoder; + + + protected PolygonComponent2D(double[] Xs, double[] Ys, RectangleComponent2D box, Component2D holes, ShapeField.Decoder decoder) { + this.Xs = Xs; + this.Ys = Ys; + this.holes = holes; + this.tree = EdgeTree.createTree(Xs, Ys); + this.box = box; + this.decoder = decoder; + } + + @Override + public boolean contains(int x, int y) { + if (box.contains(x, y)) { + containsBoundary.set(false); + if (tree.contains(decoder.decodeX(x), decoder.decodeY(y), containsBoundary)) { + if (holes != null && holes.contains(x, y)) { + return false; + } + return true; + } + } + return false; + } + + @Override + public Relation relate(int minX, int maxX, int minY, int maxY) { + if (box.disjoint(minX, maxX, minY, maxY)) { + return Relation.CELL_OUTSIDE_QUERY; + } + if (box.within(minX, maxX, minY, maxY)) { + return Relation.CELL_CROSSES_QUERY; + } + // check any holes + if (holes != null) { + Relation holeRelation = holes.relate(minX, maxX, minY, maxY); + if (holeRelation == Relation.CELL_CROSSES_QUERY) { + return Relation.CELL_CROSSES_QUERY; + } else if (holeRelation == Relation.CELL_INSIDE_QUERY) { + return Relation.CELL_OUTSIDE_QUERY; + } + } + // check each corner: if < 4 && > 0 are present, its cheaper than crossesSlowly + int numCorners = numberOfCorners(minX, maxX, minY, maxY); + if (numCorners == 4) { + if (tree.crossesBox(decoder.decodeX(minX), decoder.decodeX(maxX), decoder.decodeY(minY), decoder.decodeY(maxY), false)) { + return Relation.CELL_CROSSES_QUERY; + } + return Relation.CELL_INSIDE_QUERY; + } else if (numCorners == 0) { + if (crossesBox(decoder.decodeX(minX), decoder.decodeX(maxX), decoder.decodeY(minY), decoder.decodeY(maxY))) { + return Relation.CELL_CROSSES_QUERY; + } + return Relation.CELL_OUTSIDE_QUERY; + } + return Relation.CELL_CROSSES_QUERY; + } + + private boolean crossesBox(double minX, double maxX, double minY, double maxY) { + return containsPoint(tree.x1, tree.y1, minX, maxX, minY, maxY) || + tree.crossesBox(minX, maxX, minY, maxY, false); + } + + /** returns true if this {@link RectangleComponent2D} contains the encoded lat lon point */ + public static boolean containsPoint(final double x, final double y, final double minX, final double maxX, final double minY, final double maxY) { + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + @Override + public Relation relateTriangle(int minX, int maxX, int minY, int maxY, int aX, int aY, int bX, int bY, int cX, int cY) { + if (box.disjoint(minX, maxX, minY, maxY)) { + return Relation.CELL_OUTSIDE_QUERY; + } + if (holes != null) { + Relation holeRelation = holes.relateTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY); + if (holeRelation == Relation.CELL_CROSSES_QUERY) { + return Relation.CELL_CROSSES_QUERY; + } else if (holeRelation == Relation.CELL_INSIDE_QUERY) { + return Relation.CELL_OUTSIDE_QUERY; + } + } + if (aX == bX && bX == cX && aY == bY && bY == cY) { + // indexed "triangle" is a point: shortcut by checking contains + return contains(aX, aY) ? Relation.CELL_INSIDE_QUERY : Relation.CELL_OUTSIDE_QUERY; + } else if ((aX == cX && aY == cY) || (bX == cX && bY == cY)) { + // indexed "triangle" is a line segment: shortcut by calling appropriate method + return relateIndexedLineSegment(aX, aY, bX, bY); + } + // indexed "triangle" is a triangle: + return relateIndexedTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY); + } + + /** relates an indexed line segment (a "flat triangle") with the polygon */ + private Relation relateIndexedLineSegment(int a2x, int a2y, int b2x, int b2y) { + // check endpoints of the line segment + int numCorners = 0; + if (contains(a2x, a2y)) { + ++numCorners; + } + if (contains(b2x, b2y)) { + ++numCorners; + } + + if (numCorners == 2) { + if (tree.crossesLine(decoder.decodeX(a2x), decoder.decodeY(a2y), decoder.decodeX(b2x), decoder.decodeY(b2y))) { + return Relation.CELL_CROSSES_QUERY; + } + return Relation.CELL_INSIDE_QUERY; + } else if (numCorners == 0) { + if (tree.crossesLine(decoder.decodeX(a2x), decoder.decodeY(a2y), decoder.decodeX(b2x), decoder.decodeY(b2y))) { + return Relation.CELL_CROSSES_QUERY; + } + return Relation.CELL_OUTSIDE_QUERY; + } + return Relation.CELL_CROSSES_QUERY; + } + + /** relates an indexed triangle with the polygon */ + private Relation relateIndexedTriangle(int minX, int maxX, int minY, int maxY, int aX, int aY, int bX, int bY, int cX, int cY) { + // check each corner: if < 3 && > 0 are present, its cheaper than crossesSlowly + int numCorners = numberOfTriangleCorners(aX, aY, bX, bY, cX, cY); + if (numCorners == 3) { + if (tree.crossesTriangle(decoder.decodeX(minX), decoder.decodeX(maxX), decoder.decodeY(minY), decoder.decodeY(maxY), + decoder.decodeX(aX), decoder.decodeY(aY), decoder.decodeX(bX), decoder.decodeY(bY), decoder.decodeX(cX), decoder.decodeY(cY))) { + return Relation.CELL_CROSSES_QUERY; + } + return Relation.CELL_INSIDE_QUERY; + } else if (numCorners == 0) { + if (crossesTriangle(decoder.decodeX(minX), decoder.decodeX(maxX), decoder.decodeY(minY), decoder.decodeY(maxY), + decoder.decodeX(aX), decoder.decodeY(aY), decoder.decodeX(bX), decoder.decodeY(bY), decoder.decodeX(cX), decoder.decodeY(cY))) { + return Relation.CELL_CROSSES_QUERY; + } + return Relation.CELL_OUTSIDE_QUERY; + } + return Relation.CELL_CROSSES_QUERY; + } + + private boolean crossesTriangle(double minX, double maxX, double minY, double maxY, double aX, double aY, double bX, double bY, double cX, double cY) { + return Component2D.pointInTriangle(minX, maxX, minY, maxY, tree.x1, tree.y1, aX, aY, bX, bY, cX, cY) == true || + tree.crossesTriangle(minX, maxX, minY, maxY, aX, aY, bX, bY, cX, cY); + } + + private int numberOfTriangleCorners(int aX, int aY, int bX, int bY, int cX, int cY) { + int containsCount = 0; + if (contains(aX, aY)) { + containsCount++; + } + if (contains(bX, bY)) { + containsCount++; + } + if (containsCount == 1) { + return containsCount; + } + if (contains(cX, cY)) { + containsCount++; + } + return containsCount; + } + + // returns 0, 4, or something in between + private int numberOfCorners(int minX, int maxX, int minY, int maxY) { + int containsCount = 0; + if (contains(minX, minY)) { + containsCount++; + } + if (contains(maxX, minY)) { + containsCount++; + } + if (containsCount == 1) { + return containsCount; + } + if (contains(maxX, maxY)) { + containsCount++; + } + if (containsCount == 2) { + return containsCount; + } + if (contains(minX, maxY)) { + containsCount++; + } + return containsCount; + } + + @Override + public RectangleComponent2D getBoundingBox() { + return box; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PolygonComponent2D polygonComponent = (PolygonComponent2D) o; + return Arrays.equals(Xs, polygonComponent.Xs) && Arrays.equals(Ys, polygonComponent.Ys); + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Arrays.hashCode(Xs); + result = (int) (temp ^ (temp >>> 32)); + temp = Arrays.hashCode(Ys); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "PolygonComponent2D{" + + "Xs=" + Arrays.toString(Xs) + ", Ys=" + Arrays.toString(Ys) + + '}'; + } +} \ No newline at end of file diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/RectangleComponent2D.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/RectangleComponent2D.java new file mode 100644 index 000000000000..c8977c5cdf98 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/RectangleComponent2D.java @@ -0,0 +1,194 @@ +/* + * 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.component2D; + +import org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.index.PointValues; + + +/** Represents a 2D rectangle. + * + * @lucene.internal + * */ +public class RectangleComponent2D implements Component2D { + /** maximum X value */ + public final int minX; + /** minimum X value */ + public final int maxX; + /** maximum Y value */ + public final int minY; + /** minimum Y value */ + public final int maxY; + + + private RectangleComponent2D(int minX, int maxX, int minY, int maxY) { + this.minX = minX; + this.maxX = maxX; + this.minY = minY; + this.maxY = maxY; + assert maxX >= minX; + assert maxY >= maxY; + } + + @Override + public boolean contains(int x, int y) { + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + @Override + public PointValues.Relation relate(int minX, int maxX, int minY, int maxY) { + if (disjoint(minX, maxX, minY, maxY)) { + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + if (contains(minX, maxX, minY, maxY)) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } + return PointValues.Relation.CELL_CROSSES_QUERY; + } + + @Override + public PointValues.Relation relateTriangle(int minX, int maxX, int minY, int maxY, int aX, int aY, int bX, int bY, int cX, int cY) { + if (disjoint(minX, maxX, minY, maxY)) { + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + int numCorners = 0; + if (contains(aX, aY)) { + ++numCorners; + } + if (contains(bX, bY)) { + ++numCorners; + } + if (contains(cX, cY)) { + ++numCorners; + } + + if (numCorners == 3) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } else if (numCorners == 0) { + if (Component2D.pointInTriangle(minX, maxX, minY, maxY, this.minX, this.minY, aX, aY, bX, bY, cX, cY) || + intersectsEdge(aX, aY, bX, bY) || intersectsEdge(bX, bY, cX, cY) || intersectsEdge(cX, cY, aX, aY)) { + return PointValues.Relation.CELL_CROSSES_QUERY; + } + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + return PointValues.Relation.CELL_CROSSES_QUERY; + } + + @Override + public RectangleComponent2D getBoundingBox() { + return this; + } + + /** returns true if this {@link RectangleComponent2D} is disjoint with the provided rectangle (defined by minX, maxX, minY, maxY) */ + public boolean disjoint(final int minX, final int maxX, final int minY, final int maxY) { + return (maxX < this.minX || minX > this.maxX || maxY < this.minY || minY > this.maxY); + } + + /** returns true if this {@link RectangleComponent2D} is within the rectangle (defined by minX, maxX, minY, maxY) */ + public boolean within(final int minX, final int maxX, final int minY, final int maxY) { + return minX <= this.minX && maxX >= this.maxX && minY <= this.minY && maxY >= this.maxY; + } + + /** returns true if this {@link RectangleComponent2D} contains the rectangle (defined by minX, maxX, minY, maxY) */ + public boolean contains(final int minX, final int maxX, final int minY, final int maxY) { + return minX >= this.minX && maxX <= this.maxX && minY >= this.minY && maxY <= this.maxY; + } + + /** returns true if the edge (defined by (aX, aY) (bX, bY)) intersects the query */ + private boolean intersectsEdge(int aX, int aY, int bX, int bY) { + // shortcut: check bboxes of edges are disjoint + if (disjoint(Math.min(aX, bX), Math.max(aX, bX), Math.min(aY, bY), Math.max(aY, bY))) { + return false; + } + + // top + if (GeoUtils.orient(aX, aY, bX, bY, minX, maxY) * GeoUtils.orient(aX, aY, bX, bY, maxX, maxY) <= 0 && + GeoUtils.orient(minX, maxY, maxX, maxY, aX, aY) * GeoUtils.orient(minX, maxY, maxX, maxY, bX, bY) <= 0) { + return true; + } + + // right + if (GeoUtils.orient(aX, aY, bX, bY, maxX, maxY) * GeoUtils.orient(aX, aY, bX, bY, maxX, minY) <= 0 && + GeoUtils.orient(maxX, maxY, maxX, minY, aX, aY) * GeoUtils.orient(maxX, maxY, maxX, minY, bX, bY) <= 0) { + return true; + } + + // bottom + if (GeoUtils.orient(aX, aY, bX, bY, maxX, minY) * GeoUtils.orient(aX, aY, bX, bY, minX, minY) <= 0 && + GeoUtils.orient(maxX, minY, minX, minY, aX, aY) * GeoUtils.orient(maxX, minY, minX, minY, bX, bY) <= 0) { + return true; + } + + // left + if (GeoUtils.orient(aX, aY, bX, bY, minX, minY) * GeoUtils.orient(aX, aY, bX, bY, minX, maxY) <= 0 && + GeoUtils.orient(minX, minY, minX, maxY, aX, aY) * GeoUtils.orient(minX, minY, minX, maxY, bX, bY) <= 0) { + return true; + } + return false; + } + + /** returns true if this {@link RectangleComponent2D} contains the encoded lat lon point */ + public static boolean containsPoint(final int x, final int y, final int minX, final int maxX, final int minY, final int maxY) { + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RectangleComponent2D rectangle = (RectangleComponent2D) o; + return minX == rectangle.minX && + maxX == rectangle.maxX && + minY == rectangle.minY && + maxY == rectangle.maxY; + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Integer.hashCode(minX); + result = (int) (temp ^ (temp >>> 32)); + temp = Integer.hashCode(maxX); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Integer.hashCode(minY); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Integer.hashCode(maxY); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append("RectangleComponent2D(lat="); + b.append(minX); + b.append(" TO "); + b.append(maxX); + b.append(" lon="); + b.append(minY); + b.append(" TO "); + b.append(maxY); + b.append(")"); + return b.toString(); + } + + /** Builds a Component2D from a rectangle */ + protected static RectangleComponent2D createComponent(int minX, int maxX, int minY, int maxY) { + return new RectangleComponent2D(minX, maxX, minY, maxY); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/XYComponent2DFactory.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/XYComponent2DFactory.java new file mode 100644 index 000000000000..603cf5a1d9bb --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/XYComponent2DFactory.java @@ -0,0 +1,157 @@ +/* + * 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.component2D; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.lucene.document.XYShape; +import org.apache.lucene.geo.XYEncodingUtils; +import org.apache.lucene.geo.XYLine; +import org.apache.lucene.geo.XYPolygon; +import org.apache.lucene.geo.XYRectangle; + +/** Factory methods for creating {@link Component2D} from XY objects. + * + * @lucen.internal + */ + +public class XYComponent2DFactory { + + /** Builds a Component2D from multi-point on the form [X,Y] */ + public static Component2D create(double[]... points) { + if (points.length == 1) { + return PointComponent2D.createComponent(XYEncodingUtils.encode(points[0][0]), XYEncodingUtils.encode(points[0][1])); + } + Component2D components[] = new Component2D[points.length]; + for (int i = 0; i < components.length; i++) { + components[i] = PointComponent2D.createComponent(XYEncodingUtils.encode(points[i][0]), XYEncodingUtils.encode(points[i][1])); + } + return Component2DTree.create(components); + } + + /** Builds a Component2D from multi-rectangle */ + public static Component2D create(XYRectangle... rectangles) { + if (rectangles.length == 1) { + int minX = XYEncodingUtils.encode(rectangles[0].minX); + int maxX = XYEncodingUtils.encode(rectangles[0].maxX); + int minY = XYEncodingUtils.encode(rectangles[0].minY); + int maxY = XYEncodingUtils.encode(rectangles[0].maxY); + return RectangleComponent2D.createComponent(minX, maxX, minY, maxY); + } + List components = new ArrayList<>(); + for (XYRectangle rectangle: rectangles) { + int minX = XYEncodingUtils.encode(rectangle.minX); + int maxX = XYEncodingUtils.encode(rectangle.maxX); + int minY = XYEncodingUtils.encode(rectangle.minY); + int maxY = XYEncodingUtils.encode(rectangle.maxY); + components.add(RectangleComponent2D.createComponent(minX, maxX, minY, maxY)); + } + return Component2DTree.create(components.toArray(new Component2D[components.size()])); + } + + /** Builds a Component2D from polygon */ + private static Component2D createComponent(XYPolygon polygon) { + XYPolygon gonHoles[] = polygon.getHoles(); + Component2D holes = null; + if (gonHoles.length > 0) { + holes = create(gonHoles); + } + RectangleComponent2D box = RectangleComponent2D.createComponent(XYEncodingUtils.encode(polygon.minX), + XYEncodingUtils.encode(polygon.maxX), + XYEncodingUtils.encode(polygon.minY), + XYEncodingUtils.encode(polygon.maxY)); + return new PolygonComponent2D(quantize(polygon.getPolyX()), quantize(polygon.getPolyY()), box, holes, XYShape.DECODER); + } + + /** Builds a Component2D tree from multipolygon */ + public static Component2D create(XYPolygon... polygons) { + if (polygons.length == 1) { + return createComponent(polygons[0]); + } + Component2D components[] = new Component2D[polygons.length]; + for (int i = 0; i < components.length; i++) { + components[i] = createComponent(polygons[i]); + } + return Component2DTree.create(components); + } + + /** Builds a Component2D from line */ + private static Component2D createComponent(XYLine line) { + RectangleComponent2D box = RectangleComponent2D.createComponent(XYEncodingUtils.encode(line.minX), + XYEncodingUtils.encode(line.maxX), + XYEncodingUtils.encode(line.minY), + XYEncodingUtils.encode(line.maxY)); + return new LineComponent2D(quantize(line.getX()), quantize(line.getY()), box, XYShape.DECODER); + } + + /** Builds a Component2D tree from multiline */ + public static Component2D create(XYLine... lines) { + if (lines.length == 1) { + return createComponent(lines[0]); + } + Component2D components[] = new Component2D[lines.length]; + for (int i = 0; i < components.length; i++) { + components[i] = createComponent(lines[i]); + } + return Component2DTree.create(components); + } + + /** Builds a Component2D from polygon */ + private static Component2D createComponent(Object shape) { + if (shape instanceof double[]) { + return create((double[]) shape); + } else if (shape instanceof XYPolygon) { + return create((XYPolygon) shape); + } else if (shape instanceof XYLine) { + return create((XYLine) shape); + } else if (shape instanceof XYRectangle) { + return create((XYRectangle) shape); + } else { + throw new IllegalArgumentException("Unknown shape type: " + shape.getClass()); + } + } + + /** Builds a Component2D from an array of shape descriptors. Current descriptors supported are: + * {@link XYPolygon}, {@link XYLine}, {@link XYRectangle} and double[] ([Lat, Lon] point). + * */ + public static Component2D create(Object... shapes) { + if (shapes.length == 1) { + return createComponent(shapes[0]); + } + Component2D[] components = new Component2D[shapes.length]; + for (int i = 0; i < shapes.length; i ++) { + components[i] = createComponent(shapes[i]); + } + return Component2DTree.createTree(components, 0, components.length - 1, true); + } + + private static double[] quantize(double[] vars) { + double[] encoded = new double[vars.length]; + for (int i = 0; i < vars.length; i++) { + encoded[i] = XYEncodingUtils.decode(XYEncodingUtils.encode(vars[i])); + } + return encoded; + } + + /** Builds a Component predicate for fast computation of point in component. The component must + * be created with one of the methods of this factory */ + public static XYComponent2DPredicate createComponentPredicate(Component2D component) { + return XYComponent2DPredicate.createComponentPredicate(component); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/component2D/XYComponent2DPredicate.java b/lucene/sandbox/src/java/org/apache/lucene/component2D/XYComponent2DPredicate.java new file mode 100644 index 000000000000..369df3850344 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/component2D/XYComponent2DPredicate.java @@ -0,0 +1,154 @@ +/* + * 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.component2D; + +import java.util.function.Function; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.XYEncodingUtils; +import org.apache.lucene.index.PointValues; + +/** + * + * A component2D predicate for fast computation of point in component computation. + * + * @lucene.internal + */ +class XYComponent2DPredicate { + + private final Component2D component; + + static final int ARITY = 64; + + final int yShift, xShift; + final int yBase, xBase; + final int maxYDelta, maxXDelta; + final byte[] relations; + + private XYComponent2DPredicate( + int yShift, int xShift, + int yBase, int xBase, + int maxYDelta, int maxXDelta, + byte[] relations, + Component2D component) { + if (yShift < 1 || yShift > 31) { + throw new IllegalArgumentException(); + } + if (xShift < 1 || xShift > 31) { + throw new IllegalArgumentException(); + } + this.yShift = yShift; + this.xShift = xShift; + this.yBase = yBase; + this.xBase = xBase; + this.maxYDelta = maxYDelta; + this.maxXDelta = maxXDelta; + this.relations = relations; + this.component = component; + } + + /** Check whether the given point is within the considered component. + * NOTE: this operates directly on the encoded representation of points. */ + public boolean test(int x, int y) { + final int y2 = ((y - XYEncodingUtils.MIN_ENC_VAL) >>> yShift); + if (y2 < yBase || y2 >= yBase + maxYDelta) { + return false; + } + int x2 = ((x - XYEncodingUtils.MIN_ENC_VAL) >>> xShift); + if (x2 < xBase || x2 - xBase >= maxXDelta) { + return false; + } + final int relation = relations[(y2 - yBase) * maxXDelta + (x2 - xBase)]; + if (relation == PointValues.Relation.CELL_CROSSES_QUERY.ordinal()) { + return component.contains(x, y); + } else { + return relation == PointValues.Relation.CELL_INSIDE_QUERY.ordinal(); + } + } + + private static XYComponent2DPredicate createSubBoxes(RectangleComponent2D boundingBox, Function boxToRelation, Component2D component) { + final int minY = boundingBox.minY; + final int maxY = boundingBox.maxY; + final int minX = boundingBox.minX; + final int maxX = boundingBox.maxX; + + final int yShift, xShift; + final int yBase, xBase; + final int maxYDelta, maxXDelta; + { + long minY2 = (long) minY - XYEncodingUtils.MIN_ENC_VAL; + long maxY2 = (long) maxY - XYEncodingUtils.MIN_ENC_VAL; + yShift = computeShift(minY2, maxY2); + yBase = (int) (minY2 >>> yShift); + maxYDelta = (int) (maxY2 >>> yShift) - yBase + 1; + assert maxYDelta > 0; + } + { + long minX2 = (long) minX - XYEncodingUtils.MIN_ENC_VAL; + long maxX2 = (long) maxX - XYEncodingUtils.MIN_ENC_VAL; + xShift = computeShift(minX2, maxX2); + xBase = (int) (minX2 >>> xShift); + maxXDelta = (int) (maxX2 >>> xShift) - xBase + 1; + assert maxXDelta > 0; + } + + final byte[] relations = new byte[maxYDelta * maxXDelta]; + for (int i = 0; i < maxYDelta; ++i) { + for (int j = 0; j < maxXDelta; ++j) { + final int boxMinY = ((yBase + i) << yShift) + XYEncodingUtils.MIN_ENC_VAL; + final int boxMinX = ((xBase + j) << xShift) + XYEncodingUtils.MIN_ENC_VAL; + final int boxMaxY = boxMinY + (1 << yShift) - 1; + final int boxMaxX = boxMinX + (1 << xShift) - 1; + relations[i * maxXDelta + j] = (byte) boxToRelation.apply(RectangleComponent2D.createComponent( + boxMinX, boxMaxX < boxMinX ? XYEncodingUtils.MAX_ENC_VAL : boxMaxX, + boxMinY, boxMaxY < boxMinY ? XYEncodingUtils.MAX_ENC_VAL : boxMaxY )).ordinal(); + } + } + + return new XYComponent2DPredicate( + yShift, xShift, + yBase, xBase, + maxYDelta, maxXDelta, + relations, component); + } + + /** Compute the minimum shift value so that + * {@code (b>>>shift)-(a>>>shift)} is less that {@code ARITY}. */ + private static int computeShift(long a, long b) { + assert a <= b; + // We enforce a shift of at least 1 so that when we work with unsigned ints + // by doing (lat - MIN_VALUE), the result of the shift (lat - MIN_VALUE) >>> shift + // can be used for comparisons without particular care: the sign bit has + // been cleared so comparisons work the same for signed and unsigned ints + for (int shift = 1; ; ++shift) { + final long delta = (b >>> shift) - (a >>> shift); + if (delta >= 0 && delta < XYComponent2DPredicate.ARITY) { + return shift; + } + } + } + + /** Create a predicate that checks whether points are within a component2D. + * @lucene.internal */ + static XYComponent2DPredicate createComponentPredicate(Component2D component) { + final RectangleComponent2D boundingBox = component.getBoundingBox(); + final Function boxToRelation = box -> component.relate( + box.minX, box.maxX, box.minY, box.maxY); + return createSubBoxes(boundingBox, boxToRelation, component); + } +} 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 cd5405904317..59d8316461d1 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java @@ -21,9 +21,11 @@ import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc import org.apache.lucene.document.ShapeField.Triangle; +import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Tessellator; +import org.apache.lucene.geo.XYEncodingUtils; import org.apache.lucene.index.PointValues; // javadoc import org.apache.lucene.search.Query; @@ -54,6 +56,8 @@ */ public class LatLonShape { + public static ShapeField.Decoder DECODER = new Decoder(); + // no instance: private LatLonShape() { } @@ -109,4 +113,37 @@ public static Query newLineQuery(String field, QueryRelation queryRelation, Line public static Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) { return new LatLonShapePolygonQuery(field, queryRelation, polygons); } + + private static class Decoder implements ShapeField.Decoder { + + @Override + public double decodeX(int x) { + return GeoEncodingUtils.decodeLongitude(x); + } + + @Override + public double decodeY(int y) { + return GeoEncodingUtils.decodeLatitude(y); + } + + @Override + public int getMaxXEncodedValue() { + return GeoEncodingUtils.MAX_LON_ENCODED; + } + + @Override + public int getMinXEncodedValue() { + return GeoEncodingUtils.MIN_LON_ENCODED; + } + + @Override + public int getMaxYEncodedValue() { + return GeoEncodingUtils.encodeLatitude(90); + } + + @Override + public int getMinYEncodedValue() { + return GeoEncodingUtils.encodeLatitude(-90); + } + } } diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/ShapeField.java b/lucene/sandbox/src/java/org/apache/lucene/document/ShapeField.java index e4e9eaa04fe5..6f7792856ccb 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/ShapeField.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/ShapeField.java @@ -304,4 +304,14 @@ public static void decodeTriangle(byte[] t, int[] 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; } + + public interface Decoder { + double decodeX(int x); + double decodeY(int y); + int getMaxXEncodedValue(); + int getMinXEncodedValue(); + int getMaxYEncodedValue(); + int getMinYEncodedValue(); + + } } diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java b/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java index d55356fb8077..8804139a4b95 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/XYShape.java @@ -22,6 +22,7 @@ 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.geo.XYEncodingUtils; import org.apache.lucene.index.PointValues; // javadoc import org.apache.lucene.geo.XYLine; import org.apache.lucene.geo.XYPolygon; @@ -51,6 +52,8 @@ */ public class XYShape { + public static ShapeField.Decoder DECODER = new Decoder(); + // no instance: private XYShape() { } @@ -100,4 +103,37 @@ public static Query newLineQuery(String field, QueryRelation queryRelation, XYLi public static Query newPolygonQuery(String field, QueryRelation queryRelation, XYPolygon... polygons) { return new XYShapePolygonQuery(field, queryRelation, polygons); } + + private static class Decoder implements ShapeField.Decoder { + + @Override + public double decodeX(int x) { + return XYEncodingUtils.decode(x); + } + + @Override + public double decodeY(int y) { + return XYEncodingUtils.decode(y); + } + + @Override + public int getMaxXEncodedValue() { + return XYEncodingUtils.encode(XYEncodingUtils.MAX_VAL_INCL); + } + + @Override + public int getMinXEncodedValue() { + return XYEncodingUtils.encode(XYEncodingUtils.MIN_VAL_INCL); + } + + @Override + public int getMaxYEncodedValue() { + return XYEncodingUtils.encode(XYEncodingUtils.MAX_VAL_INCL); + } + + @Override + public int getMinYEncodedValue() { + return XYEncodingUtils.encode(XYEncodingUtils.MIN_VAL_INCL); + } + } } diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/XYEncodingUtils.java b/lucene/sandbox/src/java/org/apache/lucene/geo/XYEncodingUtils.java index 025504fd1e85..30ac4c3b2870 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/geo/XYEncodingUtils.java +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/XYEncodingUtils.java @@ -28,6 +28,8 @@ public final class XYEncodingUtils { public static final double MIN_VAL_INCL = -Float.MAX_VALUE; public static final double MAX_VAL_INCL = Float.MAX_VALUE; + public static final int MIN_ENC_VAL = encode(MIN_VAL_INCL); + public static final int MAX_ENC_VAL = encode(MAX_VAL_INCL); // No instance: private XYEncodingUtils() { diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestBaseComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestBaseComponent2D.java new file mode 100644 index 000000000000..dad1ec08ae98 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestBaseComponent2D.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.component2D; + + +import org.apache.lucene.index.PointValues; +import org.apache.lucene.util.LuceneTestCase; + +public abstract class TestBaseComponent2D extends LuceneTestCase { + + protected abstract Object nextShape(); + + protected abstract Component2D getComponent(Object shape); + + protected abstract Component2D getComponentInside(Component2D component); + + protected abstract int nextEncodedX(); + + protected abstract int nextEncodedY(); + + public void testEqualsAndHashcode() { + Object shape = nextShape(); + Component2D component1 = getComponent(shape); + Component2D component2 = getComponent(shape); + assertEquals(component1, component2); + assertEquals(component1.hashCode(), component2.hashCode()); + Object otherShape = nextShape(); + // shapes can be different but equal in the encoded space + Component2D component3 = getComponent(otherShape); + if (component1.equals(component3)) { + assertEquals(component1.hashCode(), component3.hashCode()); + } else { + assertNotEquals(component1.hashCode(), component3.hashCode()); + } + } + + public void testRandomContains() { + Object rectangle = nextShape(); + for (int i = 0; i < 5; i++) { + Component2D component = getComponent(rectangle); + Component2D insideComponent = getComponentInside(component); + if (insideComponent == null) { + continue; + } + for (int j = 0; j < 500; j++) { + int x = nextEncodedX(); + int y = nextEncodedY(); + if (insideComponent.contains(x, y)) { + assertTrue(component.contains(x, y)); + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(x, y, x, y, x, y)); + } + } + } + } + + public void testRandomTriangles() { + Object rectangle = nextShape(); + Component2D component = getComponent(rectangle); + + for (int i =0; i < 100; i++) { + int ax = nextEncodedX(); + int ay = nextEncodedY(); + int bx = nextEncodedX(); + int by = nextEncodedY(); + int cx = nextEncodedX(); + int cy = nextEncodedY(); + + int tMinX = StrictMath.min(StrictMath.min(ax, bx), cx); + int tMaxX = StrictMath.max(StrictMath.max(ax, bx), cx); + int tMinY = StrictMath.min(StrictMath.min(ay, by), cy); + int tMaxY = StrictMath.max(StrictMath.max(ay, by), cy); + + PointValues.Relation r = component.relate(tMinX, tMaxX, tMinY, tMaxY); + if (r == PointValues.Relation.CELL_OUTSIDE_QUERY) { + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relateTriangle(ax, ay, bx, by, cx, cy)); + assertFalse(component.contains(ax, ay)); + assertFalse(component.contains(bx, by)); + assertFalse(component.contains(cx, cy)); + } + else if (r == PointValues.Relation.CELL_INSIDE_QUERY) { + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(ax, ay, bx, by, cx, cy)); + assertTrue(component.contains(ax, ay)); + assertTrue(component.contains(bx, by)); + assertTrue(component.contains(cx, cy)); + } + } + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestBaseLatLonComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestBaseLatLonComponent2D.java new file mode 100644 index 000000000000..2d234aa300d3 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestBaseLatLonComponent2D.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.component2D; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.Rectangle; +import org.apache.lucene.index.PointValues; + +public abstract class TestBaseLatLonComponent2D extends TestBaseComponent2D { + + @Override + protected Component2D getComponentInside(Component2D component) { + for (int i =0; i < 500; i++) { + Rectangle rectangle = GeoTestUtil.nextBoxNotCrossingDateline(); + // allowed to conservatively return false + if (component.relate(GeoEncodingUtils.encodeLongitude(rectangle.minLon), GeoEncodingUtils.encodeLongitude(rectangle.maxLon), + GeoEncodingUtils.encodeLatitude(rectangle.minLat), GeoEncodingUtils.encodeLatitude(rectangle.maxLat)) == PointValues.Relation.CELL_INSIDE_QUERY) { + return LatLonComponent2DFactory.create(rectangle); + } + } + return null; + } + + @Override + protected int nextEncodedX() { + return GeoEncodingUtils.encodeLongitude(GeoTestUtil.nextLongitude()); + } + + @Override + protected int nextEncodedY() { + return GeoEncodingUtils.encodeLatitude(GeoTestUtil.nextLatitude()); + } + + public void testComponentPredicate() { + Object shape = nextShape(); + Component2D component = getComponent(shape); + LatLonComponent2DPredicate predicate = LatLonComponent2DFactory.createComponentPredicate(component); + for (int i =0; i < 1000; i++) { + int x = nextEncodedX(); + int y = nextEncodedY(); + assertEquals(component.contains(x, y), predicate.test(x, y)); + if (component.contains(x, y)) { + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(x, y, x, y, x, y)); + } else { + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relateTriangle(x, y, x, y, x, y)); + } + } + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestBaseXYComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestBaseXYComponent2D.java new file mode 100644 index 000000000000..4d4d2a11d070 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestBaseXYComponent2D.java @@ -0,0 +1,66 @@ +/* + * 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.component2D; + +import org.apache.lucene.geo.ShapeTestUtil; +import org.apache.lucene.geo.XYEncodingUtils; +import org.apache.lucene.geo.XYRectangle; +import org.apache.lucene.index.PointValues; + +public abstract class TestBaseXYComponent2D extends TestBaseComponent2D { + + @Override + protected Component2D getComponentInside(Component2D component) { + for (int i =0; i < 500; i++) { + XYRectangle rectangle = ShapeTestUtil.nextBox(); + // allowed to conservatively return false + if (component.relate(XYEncodingUtils.encode(rectangle.minX), XYEncodingUtils.encode(rectangle.maxX), + XYEncodingUtils.encode(rectangle.minY), XYEncodingUtils.encode(rectangle.maxY)) == PointValues.Relation.CELL_INSIDE_QUERY) { + return XYComponent2DFactory.create(rectangle); + } + } + return null; + } + + @Override + protected int nextEncodedX() { + return XYEncodingUtils.encode(ShapeTestUtil.nextDouble()); + } + + @Override + protected int nextEncodedY() { + return XYEncodingUtils.encode(ShapeTestUtil.nextDouble()); + } + + public void testComponentPredicate() { + Object shape = nextShape(); + Component2D component = getComponent(shape); + XYComponent2DPredicate predicate = XYComponent2DFactory.createComponentPredicate(component); + for (int i =0; i < 1000; i++) { + int x = nextEncodedX(); + int y = nextEncodedY(); + assertEquals(component.contains(x, y), predicate.test(x, y)); + if (component.contains(x, y)) { + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(x, y, x, y, x, y)); + } else { + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relateTriangle(x, y, x, y, x, y)); + } + } + } + +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonComponent2DTree.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonComponent2DTree.java new file mode 100644 index 000000000000..bb395dba1b7f --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonComponent2DTree.java @@ -0,0 +1,87 @@ +/* + * 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.component2D; + +import org.apache.lucene.document.BaseLatLonShapeTestCase; +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.index.PointValues; +import org.apache.lucene.util.TestUtil; + +public class TestLatLonComponent2DTree extends TestBaseLatLonComponent2D { + + @Override + protected Object nextShape() { + int numComponents = TestUtil.nextInt(random(), 2, 10); + Object[] components = new Object[numComponents]; + for (int i =0; i < numComponents; i++) { + components[i] = createRandomShape(); + } + return components; + } + + @Override + protected Component2D getComponent(Object shape) { + return LatLonComponent2DFactory.create((Object[]) shape); + } + + private Object createRandomShape() { + int type = random().nextInt(4); + switch (type) { + case 0 : return new double[] {GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude()}; + case 1 : return GeoTestUtil.nextBox(); + case 2 : return GeoTestUtil.nextPolygon(); + case 3 : return BaseLatLonShapeTestCase.getNextLine(); + default: throw new IllegalArgumentException("Unreachable code"); + } + } + + // because currently shapes can overlap, we need different logic here + @Override + public void testRandomTriangles() { + Object rectangle = nextShape(); + Component2D component = getComponent(rectangle); + + for (int i =0; i < 100; i++) { + int ax = nextEncodedX(); + int ay = nextEncodedY(); + int bx = nextEncodedX(); + int by = nextEncodedY(); + int cx = nextEncodedX(); + int cy = nextEncodedY(); + + int tMinX = StrictMath.min(StrictMath.min(ax, bx), cx); + int tMaxX = StrictMath.max(StrictMath.max(ax, bx), cx); + int tMinY = StrictMath.min(StrictMath.min(ay, by), cy); + int tMaxY = StrictMath.max(StrictMath.max(ay, by), cy); + + PointValues.Relation r = component.relate(tMinX, tMaxX, tMinY, tMaxY); + if (r == PointValues.Relation.CELL_OUTSIDE_QUERY) { + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relateTriangle(ax, ay, bx, by, cx, cy)); + } + if (component.contains(ax, ay)) { + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(ax, ay, ax, ay, ax, ay)); + } + if (component.contains(bx, by)) { + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(bx, by, bx, by, bx, by)); + } + if (component.contains(cx, cy)) { + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(cx, cy, cx, cy, cx, cy)); + } + } + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonLineComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonLineComponent2D.java new file mode 100644 index 000000000000..de6bb0cbfc64 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonLineComponent2D.java @@ -0,0 +1,88 @@ +/* + * 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.component2D; + +import org.apache.lucene.document.TestLatLonLineShapeQueries; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.Line; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.LuceneTestCase; + +public class TestLatLonLineComponent2D extends TestBaseLatLonComponent2D { + + @Override + protected Object nextShape() { + return TestLatLonLineShapeQueries.getNextLine(); + } + + @Override + protected Component2D getComponent(Object shape) { + if (random().nextBoolean()) { + return LatLonComponent2DFactory.create(shape); + } else { + return LatLonComponent2DFactory.create((Line) shape); + } + } + + public void testTriangleDisjoint() { + Line line = new Line(new double[] {0, 1, 2, 3}, new double[] {0, 0, 2, 2}); + Component2D component = LatLonComponent2DFactory.create(line); + int ax = GeoEncodingUtils.encodeLongitude(4); + int ay = GeoEncodingUtils.encodeLatitude(4); + int bx = GeoEncodingUtils.encodeLongitude(5); + int by = GeoEncodingUtils.encodeLatitude(5); + int cx = GeoEncodingUtils.encodeLongitude(5); + int cy = GeoEncodingUtils.encodeLatitude(4); + assertEquals(Relation.CELL_OUTSIDE_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy));; + } + + public void testTriangleIntersects() { + Line line = new Line(new double[] {0.5, 0, 1, 2, 3}, new double[] {0.5, 0, 0, 2, 2}); + Component2D component = LatLonComponent2DFactory.create(line); + int ax = GeoEncodingUtils.encodeLongitude(0.0); + int ay = GeoEncodingUtils.encodeLatitude(0.0); + int bx = GeoEncodingUtils.encodeLongitude(1); + int by = GeoEncodingUtils.encodeLatitude(0); + int cx = GeoEncodingUtils.encodeLongitude(0); + int cy = GeoEncodingUtils.encodeLatitude(1); + assertEquals(Relation.CELL_CROSSES_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + } + + public void testTriangleContains() { + Line line = new Line(new double[] {0.5, 0, 1, 2, 3}, new double[] {0.5, 0, 0, 2, 2}); + Component2D component = LatLonComponent2DFactory.create(line); + int ax = GeoEncodingUtils.encodeLongitude(-10); + int ay = GeoEncodingUtils.encodeLatitude(-10); + int bx = GeoEncodingUtils.encodeLongitude(4); + int by = GeoEncodingUtils.encodeLatitude(-10); + int cx = GeoEncodingUtils.encodeLongitude(4); + int cy = GeoEncodingUtils.encodeLatitude(30); + assertEquals(Relation.CELL_CROSSES_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + } + + public void testLineSharedLine() { + Line l = new Line(new double[] {0, 0, 0, 0}, new double[] {-2, -1, 0, 1}); + Component2D component = LatLonComponent2DFactory.create(l); + Relation r = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(-5), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(5), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(-5), GeoEncodingUtils.encodeLatitude(0)); + assertEquals(Relation.CELL_CROSSES_QUERY, r); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonPointComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonPointComponent2D.java new file mode 100644 index 000000000000..028854c5f3e6 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonPointComponent2D.java @@ -0,0 +1,107 @@ +/* + * 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.component2D; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.index.PointValues; + +public class TestLatLonPointComponent2D extends TestBaseLatLonComponent2D { + + @Override + protected Object nextShape() { + return new double[] {GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude()}; + } + + @Override + protected Component2D getComponent(Object shape) { + if (random().nextBoolean()) { + return LatLonComponent2DFactory.create(shape); + } else { + return LatLonComponent2DFactory.create((double[]) shape); + } + } + + public void testTriangleDisjoint() { + double[] point = new double[]{0, 1}; + Component2D component = LatLonComponent2DFactory.create(point); + int ax = GeoEncodingUtils.encodeLongitude(4); + int ay = GeoEncodingUtils.encodeLatitude(4); + int bx = GeoEncodingUtils.encodeLongitude(5); + int by = GeoEncodingUtils.encodeLatitude(5); + int cx = GeoEncodingUtils.encodeLongitude(5); + int cy = GeoEncodingUtils.encodeLatitude(4); + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minLat = GeoEncodingUtils.encodeLongitude(4); + int maxLat = GeoEncodingUtils.encodeLatitude(5); + int minLon = GeoEncodingUtils.encodeLongitude(4); + int maxLon = GeoEncodingUtils.encodeLatitude(5); + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relate(minLat, maxLat, minLon, maxLon)); + } + + public void testTriangleIntersects() { + double[] point = new double[]{0.5, 0.5}; + Component2D component = LatLonComponent2DFactory.create(point); + int ax = GeoEncodingUtils.encodeLongitude(0.5); + int ay = GeoEncodingUtils.encodeLatitude(0.5); + int bx = GeoEncodingUtils.encodeLongitude(2); + int by = GeoEncodingUtils.encodeLatitude(2); + int cx = GeoEncodingUtils.encodeLongitude(0.5); + int cy = GeoEncodingUtils.encodeLatitude(2); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minLat = GeoEncodingUtils.encodeLongitude(0.5); + int maxLat = GeoEncodingUtils.encodeLatitude(2); + int minLon = GeoEncodingUtils.encodeLongitude(0.5); + int maxLon = GeoEncodingUtils.encodeLatitude(2); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relate(minLat, maxLat, minLon, maxLon)); + } + + public void testTriangleWithin() { + double[] point = new double[]{0.5, 0.5}; + Component2D component = LatLonComponent2DFactory.create(point); + int ax = GeoEncodingUtils.encodeLongitude(0.5); + int ay = GeoEncodingUtils.encodeLatitude(0.5); + int bx = GeoEncodingUtils.encodeLongitude(0.5); + int by = GeoEncodingUtils.encodeLatitude(0.5); + int cx = GeoEncodingUtils.encodeLongitude(0.5); + int cy = GeoEncodingUtils.encodeLatitude(0.5); + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minX = GeoEncodingUtils.encodeLongitude(0.5); + int maxX = GeoEncodingUtils.encodeLongitude(0.5); + int minY = GeoEncodingUtils.encodeLatitude(0.5); + int maxY = GeoEncodingUtils.encodeLatitude(0.5); + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relate(minX, maxX, minY, maxY)); + } + + public void testTriangleContains() { + double[] point = new double[]{0.5, 0.5}; + Component2D component = LatLonComponent2DFactory.create(point); + int ax = GeoEncodingUtils.encodeLongitude(-60.); + int ay = GeoEncodingUtils.encodeLatitude(-1); + int bx = GeoEncodingUtils.encodeLongitude(2); + int by = GeoEncodingUtils.encodeLatitude(-1); + int cx = GeoEncodingUtils.encodeLongitude(2); + int cy = GeoEncodingUtils.encodeLatitude(60); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minX = GeoEncodingUtils.encodeLongitude(-1); + int maxX = GeoEncodingUtils.encodeLatitude(2); + int minY = GeoEncodingUtils.encodeLongitude(-1); + int maxY = GeoEncodingUtils.encodeLatitude(2); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relate(minX, maxX, minY, maxY)); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonPolygonComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonPolygonComponent2D.java new file mode 100644 index 000000000000..2836e89281f5 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonPolygonComponent2D.java @@ -0,0 +1,503 @@ +/* + * 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.component2D; + +import com.carrotsearch.randomizedtesting.generators.RandomNumbers; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.Rectangle; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.TestUtil; + +import static org.apache.lucene.geo.GeoTestUtil.createRegularPolygon; +import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; +import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; +import static org.apache.lucene.geo.GeoTestUtil.nextPointNear; +import static org.apache.lucene.geo.GeoTestUtil.nextPolygon; + +/** Test Polygon2D impl */ +public class TestLatLonPolygonComponent2D extends TestBaseLatLonComponent2D { + + @Override + protected Object nextShape() { + return GeoTestUtil.nextPolygon(); + } + + @Override + protected Component2D getComponent(Object shape) { + if (random().nextBoolean()) { + return LatLonComponent2DFactory.create(shape); + } else { + return LatLonComponent2DFactory.create((Polygon) shape); + } + } + + /** Three boxes, an island inside a hole inside a shape */ + public void testMultiPolygon() { + Polygon hole = new Polygon(new double[] { -10, -10, 10, 10, -10 }, new double[] { -10, 10, 10, -10, -10 }); + Polygon outer = new Polygon(new double[] { -50, -50, 50, 50, -50 }, new double[] { -50, 50, 50, -50, -50 }, hole); + Polygon island = new Polygon(new double[] { -5, -5, 5, 5, -5 }, new double[] { -5, 5, 5, -5, -5 } ); + Component2D polygon = LatLonComponent2DFactory.create(outer, island); + + // contains(point) + assertTrue(polygon.contains(GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(-2))); // on the island + assertFalse(polygon.contains(GeoEncodingUtils.encodeLongitude(6), GeoEncodingUtils.encodeLatitude(-6))); // in the hole + assertTrue(polygon.contains(GeoEncodingUtils.encodeLongitude(25), GeoEncodingUtils.encodeLatitude(-25))); // on the mainland + assertFalse(polygon.contains(GeoEncodingUtils.encodeLongitude(51), GeoEncodingUtils.encodeLatitude(-51))); // in the ocean + + // relate(box): this can conservatively return CELL_CROSSES_QUERY + assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(GeoEncodingUtils.encodeLongitude(-2), GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(-2), GeoEncodingUtils.encodeLatitude(2))); // on the island + assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(GeoEncodingUtils.encodeLongitude(6), GeoEncodingUtils.encodeLongitude(7), GeoEncodingUtils.encodeLatitude(6), GeoEncodingUtils.encodeLatitude(7))); // in the hole + assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(GeoEncodingUtils.encodeLongitude(24), GeoEncodingUtils.encodeLongitude(25), GeoEncodingUtils.encodeLatitude(24), GeoEncodingUtils.encodeLatitude(25))); // on the mainland + assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(GeoEncodingUtils.encodeLongitude(51), GeoEncodingUtils.encodeLongitude(52), GeoEncodingUtils.encodeLatitude(51), GeoEncodingUtils.encodeLatitude(52))); // in the ocean + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(GeoEncodingUtils.encodeLongitude(-60), GeoEncodingUtils.encodeLongitude(60), GeoEncodingUtils.encodeLatitude(-60), GeoEncodingUtils.encodeLatitude(60))); // enclosing us completely + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(GeoEncodingUtils.encodeLongitude(49), GeoEncodingUtils.encodeLongitude(51), GeoEncodingUtils.encodeLatitude(49), GeoEncodingUtils.encodeLatitude(51))); // overlapping the mainland + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(GeoEncodingUtils.encodeLongitude(9), GeoEncodingUtils.encodeLongitude(11), GeoEncodingUtils.encodeLatitude(9), GeoEncodingUtils.encodeLatitude(11))); // overlapping the hole + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(GeoEncodingUtils.encodeLongitude(5), GeoEncodingUtils.encodeLongitude(6), GeoEncodingUtils.encodeLatitude(5), GeoEncodingUtils.encodeLatitude(6))); // overlapping the island + } + + public void testRandomMultiPolygon() { + int length = random().nextInt(14) + 1; + Polygon[] polygons = new Polygon[length]; + Component2D[] components = new Component2D[length]; + for (int i =0; i < length; i++) { + polygons[i] = GeoTestUtil.nextPolygon(); + components[i] = LatLonComponent2DFactory.create(polygons[i]); + } + Component2D component = LatLonComponent2DFactory.create(polygons); + for (int j = 0; j < 1000; j++) { + int latitude = GeoEncodingUtils.encodeLatitude(GeoTestUtil.nextLatitude()); + int longitude = GeoEncodingUtils.encodeLongitude(GeoTestUtil.nextLongitude()); + boolean c1 = component.contains(latitude, longitude); + boolean c2 = false; + for (Component2D c : components) { + if (c.contains(latitude, longitude)) { + c2 = true; + break; + } + } + assertEquals(c1, c2); + } + } + + public void testPacMan() throws Exception { + // pacman + double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; + double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; + + // candidate crosses cell + int xMin = GeoEncodingUtils.encodeLongitude(2);//-5; + int xMax = GeoEncodingUtils.encodeLongitude(11);//0.000001; + int yMin = GeoEncodingUtils.encodeLatitude(-1);//0; + int yMax = GeoEncodingUtils.encodeLatitude(1);//5; + + // test cell crossing poly + Component2D polygon = LatLonComponent2DFactory.create(new Polygon(py, px)); + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(xMin, xMax, yMin, yMax)); + } + + public void testBoundingBox() throws Exception { + for (int i = 0; i < 100; i++) { + Component2D impl = LatLonComponent2DFactory.create(nextPolygon()); + for (int j = 0; j < 100; j++) { + int x = GeoEncodingUtils.encodeLongitude(nextLongitude()); + int y = GeoEncodingUtils.encodeLatitude(nextLatitude()); + // if the point is within poly, then it should be in our bounding box + if (impl.contains(x, y)) { + assertTrue(x >= impl.getBoundingBox().minX && x <= impl.getBoundingBox().maxX); + assertTrue(y >= impl.getBoundingBox().minY && y <= impl.getBoundingBox().maxY); + } + } + } + } + + // targets the bounding box directly + public void testBoundingBoxEdgeCases() throws Exception { + for (int i = 0; i < 100; i++) { + Polygon polygon = nextPolygon(); + Component2D impl = LatLonComponent2DFactory.create(polygon); + + for (int j = 0; j < 100; j++) { + double point[] = GeoTestUtil.nextPointNear(polygon); + int x = GeoEncodingUtils.encodeLatitude(point[0]); + int y = GeoEncodingUtils.encodeLongitude(point[1]); + // if the point is within poly, then it should be in our bounding box + if (impl.contains(x, y)) { + assertTrue(x >= impl.getBoundingBox().minX && x <= impl.getBoundingBox().maxX); + assertTrue(y >= impl.getBoundingBox().minY && y <= impl.getBoundingBox().maxY); + } + } + } + } + + /** If polygon.contains(box) returns true, then any point in that box should return true as well */ + public void testContainsRandom() throws Exception { + int iters = atLeast(50); + for (int i = 0; i < iters; i++) { + Polygon polygon = nextPolygon(); + Component2D impl = LatLonComponent2DFactory.create(polygon); + for (int j = 0; j < 100; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false + if (impl.relate(GeoEncodingUtils.encodeLongitude(rectangle.minLon), GeoEncodingUtils.encodeLongitude(rectangle.maxLon), + GeoEncodingUtils.encodeLatitude(rectangle.minLat), GeoEncodingUtils.encodeLatitude(rectangle.maxLat)) == Relation.CELL_INSIDE_QUERY) { + for (int k = 0; k < 500; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + int x = GeoEncodingUtils.encodeLongitude(point[1]); + int y = GeoEncodingUtils.encodeLatitude(point[0]); + + // check for sure its in our box + if (y >= GeoEncodingUtils.encodeLatitude(rectangle.minLat) && y <= GeoEncodingUtils.encodeLatitude(rectangle.maxLat) && + x >= GeoEncodingUtils.encodeLongitude(rectangle.minLon) && x <= GeoEncodingUtils.encodeLongitude(rectangle.maxLon)) { + assertTrue(impl.contains(x, y)); + assertEquals(Relation.CELL_INSIDE_QUERY, impl.relateTriangle(x, y, x, y, x, y)); + } + } + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + int x = GeoEncodingUtils.encodeLongitude(point[1]); + int y = GeoEncodingUtils.encodeLatitude(point[0]); + // check for sure its in our box + if (y >= GeoEncodingUtils.encodeLatitude(rectangle.minLat) && y <= GeoEncodingUtils.encodeLatitude(rectangle.maxLat) && + x >= GeoEncodingUtils.encodeLongitude(rectangle.minLon) && x <= GeoEncodingUtils.encodeLongitude(rectangle.maxLon)) { + assertTrue(impl.contains(x, y)); + assertEquals(Relation.CELL_INSIDE_QUERY, impl.relateTriangle(x, y, x, y, x, y)); + } + } + } + } + } + } + + /** If polygon.contains(box) returns true, then any point in that box should return true as well */ + // different from testContainsRandom in that its not a purely random test. we iterate the vertices of the polygon + // and generate boxes near each one of those to try to be more efficient. + public void testContainsEdgeCases() throws Exception { + for (int i = 0; i < 1000; i++) { + Polygon polygon = nextPolygon(); + Component2D impl = LatLonComponent2DFactory.create(polygon); + for (int j = 0; j < 10; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false + if (impl.relate(GeoEncodingUtils.encodeLongitude(rectangle.minLon), GeoEncodingUtils.encodeLongitude(rectangle.maxLon), + GeoEncodingUtils.encodeLatitude(rectangle.minLat), GeoEncodingUtils.encodeLatitude(rectangle.maxLat)) == Relation.CELL_INSIDE_QUERY) { + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + int x = GeoEncodingUtils.encodeLongitude(point[1]); + int y = GeoEncodingUtils.encodeLatitude(point[0]); + // check for sure its in our box + if (y >= GeoEncodingUtils.encodeLatitude(rectangle.minLat) && y <= GeoEncodingUtils.encodeLatitude(rectangle.maxLat) && + x >= GeoEncodingUtils.encodeLongitude(rectangle.minLon) && x <= GeoEncodingUtils.encodeLongitude(rectangle.maxLon)) { + assertTrue(impl.contains(x, y)); + assertEquals(Relation.CELL_INSIDE_QUERY, impl.relateTriangle(x, y, x, y, x, y)); + } + } + for (int k = 0; k < 20; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + int x = GeoEncodingUtils.encodeLongitude(point[1]); + int y = GeoEncodingUtils.encodeLatitude(point[0]); + // check for sure its in our box + if (y >= GeoEncodingUtils.encodeLatitude(rectangle.minLat) && y <= GeoEncodingUtils.encodeLatitude(rectangle.maxLat) && + x >= GeoEncodingUtils.encodeLongitude(rectangle.minLon) && x <= GeoEncodingUtils.encodeLongitude(rectangle.maxLon)) { + assertTrue(impl.contains(x, y)); + assertEquals(Relation.CELL_INSIDE_QUERY, impl.relateTriangle(x, y, x, y, x, y)); + } + } + } + } + } + } + + /** If polygon.intersects(box) returns false, then any point in that box should return false as well */ + public void testIntersectRandom() { + int iters = atLeast(10); + for (int i = 0; i < iters; i++) { + Polygon polygon = nextPolygon(); + Component2D impl = LatLonComponent2DFactory.create(polygon); + for (int j = 0; j < 100; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + if (impl.relate(GeoEncodingUtils.encodeLongitude(rectangle.minLon), GeoEncodingUtils.encodeLongitude(rectangle.maxLon), + GeoEncodingUtils.encodeLatitude(rectangle.minLat), GeoEncodingUtils.encodeLatitude(rectangle.maxLat)) == Relation.CELL_OUTSIDE_QUERY) { + for (int k = 0; k < 1000; k++) { + double point[] = GeoTestUtil.nextPointNear(rectangle); + // this tests in our range but sometimes outside! so we have to double-check its really in other box + int x = GeoEncodingUtils.encodeLongitude(point[1]); + int y = GeoEncodingUtils.encodeLatitude(point[0]); + // check for sure its in our box + if (y >= GeoEncodingUtils.encodeLatitude(rectangle.minLat) && y <= GeoEncodingUtils.encodeLatitude(rectangle.maxLat) && + x >= GeoEncodingUtils.encodeLongitude(rectangle.minLon) && x <= GeoEncodingUtils.encodeLongitude(rectangle.maxLon)) { + assertFalse(impl.contains(x, y)); + assertEquals(Relation.CELL_OUTSIDE_QUERY, impl.relateTriangle(x, y, x, y, x, y)); + } + } + for (int k = 0; k < 100; k++) { + double point[] = GeoTestUtil.nextPointNear(polygon); + // this tests in our range but sometimes outside! so we have to double-check its really in other box + int x = GeoEncodingUtils.encodeLongitude(point[1]); + int y = GeoEncodingUtils.encodeLatitude(point[0]); + // check for sure its in our box + if (y >= GeoEncodingUtils.encodeLatitude(rectangle.minLat) && y <= GeoEncodingUtils.encodeLatitude(rectangle.maxLat) && + x >= GeoEncodingUtils.encodeLongitude(rectangle.minLon) && x <= GeoEncodingUtils.encodeLongitude(rectangle.maxLon)) { + assertFalse(impl.contains(x, y)); + assertEquals(Relation.CELL_OUTSIDE_QUERY, impl.relateTriangle(x, y, x, y, x, y)); + } + } + } + } + } + } + + /** If polygon.intersects(box) returns false, then any point in that box should return false as well */ + // different from testIntersectsRandom in that its not a purely random test. we iterate the vertices of the polygon + // and generate boxes near each one of those to try to be more efficient. + public void testIntersectEdgeCases() { + for (int i = 0; i < 100; i++) { + Polygon polygon = nextPolygon(); + Component2D impl = LatLonComponent2DFactory.create(polygon); + + for (int j = 0; j < 10; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false. + if (impl.relate(GeoEncodingUtils.encodeLongitude(rectangle.minLon), GeoEncodingUtils.encodeLongitude(rectangle.maxLon), + GeoEncodingUtils.encodeLatitude(rectangle.minLat), GeoEncodingUtils.encodeLatitude(rectangle.maxLat)) == Relation.CELL_OUTSIDE_QUERY) { + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + int x = GeoEncodingUtils.encodeLongitude(point[1]); + int y = GeoEncodingUtils.encodeLatitude(point[0]); + // check for sure its in our box + if (y >= GeoEncodingUtils.encodeLatitude(rectangle.minLat) && y <= GeoEncodingUtils.encodeLatitude(rectangle.maxLat) && + x >= GeoEncodingUtils.encodeLongitude(rectangle.minLon) && x <= GeoEncodingUtils.encodeLongitude(rectangle.maxLon)) { + assertFalse(impl.contains(x, y)); + assertEquals(Relation.CELL_OUTSIDE_QUERY, impl.relateTriangle(x, y, x, y, x, y)); + } + } + for (int k = 0; k < 50; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + int x = GeoEncodingUtils.encodeLongitude(point[1]); + int y = GeoEncodingUtils.encodeLatitude(point[0]); + // check for sure its in our box + if (y >= GeoEncodingUtils.encodeLatitude(rectangle.minLat) && y <= GeoEncodingUtils.encodeLatitude(rectangle.maxLat) && + x >= GeoEncodingUtils.encodeLongitude(rectangle.minLon) && x <= GeoEncodingUtils.encodeLongitude(rectangle.maxLon)) { + assertFalse(impl.contains(x, y)); + assertEquals(Relation.CELL_OUTSIDE_QUERY, impl.relateTriangle(x, y, x, y, x, y)); + } + } + } + } + } + } + + /** Tests edge case behavior with respect to insideness */ + public void testEdgeInsideness() { + Component2D poly = LatLonComponent2DFactory.create(new Polygon(new double[] { -2, -2, 2, 2, -2 }, new double[] { -2, 2, 2, -2, -2 })); + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(-2), GeoEncodingUtils.encodeLatitude(-2))); // bottom left corner: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(-2))); // bottom right corner: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(-2), GeoEncodingUtils.encodeLatitude(2))); // top left corner: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(2))); // top right corner: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(-1), GeoEncodingUtils.encodeLatitude(-2))); // bottom side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(0), GeoEncodingUtils.encodeLatitude(-2))); // bottom side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(1), GeoEncodingUtils.encodeLatitude(-2))); // bottom side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(-1), GeoEncodingUtils.encodeLatitude(2))); // top side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(0), GeoEncodingUtils.encodeLatitude(2))); // top side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(1), GeoEncodingUtils.encodeLatitude(2))); // top side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(-1))); // right side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(0))); // right side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(1))); // right side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(-2), GeoEncodingUtils.encodeLatitude(-1))); // left side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(-2), GeoEncodingUtils.encodeLatitude(0))); // left side: true + assertTrue(poly.contains(GeoEncodingUtils.encodeLongitude(-2), GeoEncodingUtils.encodeLatitude(1))); // left side: true + } + + // targets the polygon directly + public void testRelateTriangle() { + for (int i = 0; i < 100; ++i) { + Polygon polygon = nextPolygon(); + Component2D impl = LatLonComponent2DFactory.create(polygon); + + for (int j = 0; j < 100; j++) { + double[] a = nextPointNear(polygon); + double[] b = nextPointNear(polygon); + double[] c = nextPointNear(polygon); + + int[] aEnc = new int[] {GeoEncodingUtils.encodeLatitude(a[0]), GeoEncodingUtils.encodeLongitude(a[1])}; + int[] bEnc = new int[] {GeoEncodingUtils.encodeLatitude(b[0]), GeoEncodingUtils.encodeLongitude(b[1])}; + int[] cEnc = new int[] {GeoEncodingUtils.encodeLatitude(c[0]), GeoEncodingUtils.encodeLongitude(c[1])}; + + // if the point is within poly, then triangle should not intersect + if (impl.contains(aEnc[1], aEnc[0]) || impl.contains(bEnc[1], bEnc[0]) || impl.contains(cEnc[1], cEnc[0])) { + assertTrue(impl.relateTriangle(aEnc[1], aEnc[0], bEnc[1], bEnc[0], cEnc[1], cEnc[0]) != Relation.CELL_OUTSIDE_QUERY); + } + } + } + } + + public void testRelateTriangleContainsPolygon() { + Polygon polygon = new Polygon(new double[]{0, 0, 1, 1, 0}, new double[]{0, 1, 1, 0, 0}); + Component2D impl = LatLonComponent2DFactory.create(polygon); + assertEquals(Relation.CELL_CROSSES_QUERY, impl.relateTriangle(GeoEncodingUtils.encodeLongitude(-10) , GeoEncodingUtils.encodeLatitude(-1), + GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(-1), + GeoEncodingUtils.encodeLongitude(10), GeoEncodingUtils.encodeLatitude(10))); + } + + // test + public void testRelateTriangleEdgeCases() { + for (int i = 0; i < 100; ++i) { + // random radius between 1Km and 100Km + int randomRadius = RandomNumbers.randomIntBetween(random(), 1000, 100000); + // random number of vertices + int numVertices = RandomNumbers.randomIntBetween(random(), 100, 1000); + Polygon polygon = createRegularPolygon(0, 0, randomRadius, numVertices); + Component2D impl = LatLonComponent2DFactory.create(polygon); + + // create and test a simple tessellation + for (int j = 1; j < numVertices; ++j) { + int[] a = new int[] {0, 0}; // center of poly + int[] b = new int[] {GeoEncodingUtils.encodeLatitude(polygon.getPolyLat(j - 1)), + GeoEncodingUtils.encodeLongitude(polygon.getPolyLon(j - 1))}; + // occassionally test pancake triangles + int[] c = random().nextBoolean() ? new int[] {GeoEncodingUtils.encodeLatitude(polygon.getPolyLat(j)), GeoEncodingUtils.encodeLongitude(polygon.getPolyLon(j))} : new int[] {a[0], a[1]}; + assertTrue(impl.relateTriangle(a[0], a[1], b[0], b[1], c[0], c[1]) != Relation.CELL_OUTSIDE_QUERY); + } + } + } + + public void testLineCrossingPolygonPoints() { + Polygon p = new Polygon(new double[] {0, -1, 0, 1, 0}, new double[] {-1, 0, 1, 0, -1}); + Component2D component = LatLonComponent2DFactory.create(p); + Relation rel = component.relateTriangle(GeoEncodingUtils.encodeLongitude(-1.5), + GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(1.5), + GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(-1.5), + GeoEncodingUtils.encodeLatitude(0)); + assertEquals(Relation.CELL_CROSSES_QUERY, rel); + } + + public void testRandomLineCrossingPolygon() { + Polygon p = GeoTestUtil.createRegularPolygon(0, 0, 1000, TestUtil.nextInt(random(), 100, 10000)); + Component2D polygon2D = LatLonComponent2DFactory.create(p); + for (int i=0; i < 1000; i ++) { + double longitude = GeoTestUtil.nextLongitude(); + double latitude = GeoTestUtil.nextLatitude(); + Relation rel = polygon2D.relateTriangle( + GeoEncodingUtils.encodeLongitude(-longitude), + GeoEncodingUtils.encodeLatitude(-latitude), + GeoEncodingUtils.encodeLongitude(longitude), + GeoEncodingUtils.encodeLatitude(latitude), + GeoEncodingUtils.encodeLongitude(-longitude), + GeoEncodingUtils.encodeLatitude(-latitude)); + assertNotEquals(Relation.CELL_OUTSIDE_QUERY, rel); + } + } + + public void testLUCENE8679() { + double alat = 1.401298464324817E-45; + double alon = 24.76789767911785; + double blat = 34.26468306870807; + double blon = -52.67048754768767; + Polygon polygon = new Polygon(new double[] {-14.448264200949083, 0, 0, -14.448264200949083, -14.448264200949083}, + new double[] {0.9999999403953552, 0.9999999403953552, 124.50086371762484, 124.50086371762484, 0.9999999403953552}); + Component2D component = LatLonComponent2DFactory.create(polygon); + Relation rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(alon), GeoEncodingUtils.encodeLatitude(blat), + GeoEncodingUtils.encodeLongitude(blon), GeoEncodingUtils.encodeLatitude(blat), + GeoEncodingUtils.encodeLongitude(alon), GeoEncodingUtils.encodeLatitude(alat)); + + assertEquals(Relation.CELL_CROSSES_QUERY, rel); + + rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(alon), GeoEncodingUtils.encodeLatitude(blat), + GeoEncodingUtils.encodeLongitude(alon), GeoEncodingUtils.encodeLatitude(alat), + GeoEncodingUtils.encodeLongitude(blon), GeoEncodingUtils.encodeLatitude(blat)); + + assertEquals(Relation.CELL_CROSSES_QUERY, rel); + } + + public void testTriangleTouchingEdges() { + Polygon p = new Polygon(new double[] {0, 0, 1, 1, 0}, new double[] {0, 1, 1, 0, 0}); + Component2D component = LatLonComponent2DFactory.create(p); + //3 shared points + Relation rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(0.5), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(1), GeoEncodingUtils.encodeLatitude(0.5), + GeoEncodingUtils.encodeLongitude(0.5), GeoEncodingUtils.encodeLatitude(1)); + assertEquals(Relation.CELL_INSIDE_QUERY, rel); + //2 shared points + rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(0.5), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(1), GeoEncodingUtils.encodeLatitude(0.5), + GeoEncodingUtils.encodeLongitude(0.5), GeoEncodingUtils.encodeLatitude(0.75)); + assertEquals(Relation.CELL_INSIDE_QUERY, rel); + //1 shared point + rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(0.5), GeoEncodingUtils.encodeLatitude(0.5), + GeoEncodingUtils.encodeLongitude(0.5), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(0.75), GeoEncodingUtils.encodeLatitude(0.75)); + assertEquals(Relation.CELL_INSIDE_QUERY, rel); + // 1 shared point but out + rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(1), GeoEncodingUtils.encodeLatitude(0.5), + GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(2)); + assertEquals(Relation.CELL_CROSSES_QUERY, rel); + // 1 shared point but crossing + rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(0.5), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(2), GeoEncodingUtils.encodeLatitude(0.5), + GeoEncodingUtils.encodeLongitude(0.5), GeoEncodingUtils.encodeLatitude(1)); + assertEquals(Relation.CELL_CROSSES_QUERY, rel); + //share one edge + rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(0), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(0), GeoEncodingUtils.encodeLatitude(1), + GeoEncodingUtils.encodeLongitude(0.5), GeoEncodingUtils.encodeLatitude(0.5)); + assertEquals(Relation.CELL_INSIDE_QUERY, rel); + //share one edge outside + rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(0), GeoEncodingUtils.encodeLatitude(1), + GeoEncodingUtils.encodeLongitude(1.5), GeoEncodingUtils.encodeLatitude(1.5), + GeoEncodingUtils.encodeLongitude(1), GeoEncodingUtils.encodeLatitude(1)); + assertEquals(Relation.CELL_CROSSES_QUERY, rel); + } + + public void testTriangleCrossingPolygonVertices() { + Polygon p = new Polygon(new double[] {0, 0, -5, -10, -5, 0}, new double[] {-1, 1, 5, 0, -5, -1}); + Component2D component = LatLonComponent2DFactory.create(p); + Relation rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(-5), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(10), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(-5), GeoEncodingUtils.encodeLatitude(-15)); + assertEquals(Relation.CELL_CROSSES_QUERY, rel); + } + + public void testLineCrossingPolygonVertices() { + Polygon p = new Polygon(new double[] {0, -1, 0, 1, 0}, new double[] {-1, 0, 1, 0, -1}); + Component2D component = LatLonComponent2DFactory.create(p); + Relation rel = component.relateTriangle( + GeoEncodingUtils.encodeLongitude(-1.5), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(1.5), GeoEncodingUtils.encodeLatitude(0), + GeoEncodingUtils.encodeLongitude(-1.5), GeoEncodingUtils.encodeLatitude(0)); + assertEquals(Relation.CELL_CROSSES_QUERY, rel); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonRectangleComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonRectangleComponent2D.java new file mode 100644 index 000000000000..21204a1d8888 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestLatLonRectangleComponent2D.java @@ -0,0 +1,108 @@ +/* + * 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.component2D; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.Rectangle; +import org.apache.lucene.index.PointValues; + +public class TestLatLonRectangleComponent2D extends TestBaseLatLonComponent2D { + + @Override + protected Object nextShape() { + return GeoTestUtil.nextBox(); + } + + @Override + protected Component2D getComponent(Object shape) { + if (random().nextBoolean()) { + return LatLonComponent2DFactory.create(shape); + } else { + return LatLonComponent2DFactory.create((Rectangle) shape); + } + } + + public void testTriangleDisjoint() { + Rectangle rectangle = new Rectangle(0, 1, 0, 1); + Component2D component = LatLonComponent2DFactory.create(rectangle); + int ax = GeoEncodingUtils.encodeLongitude(4); + int ay = GeoEncodingUtils.encodeLatitude(4); + int bx = GeoEncodingUtils.encodeLongitude(5); + int by = GeoEncodingUtils.encodeLatitude(5); + int cx = GeoEncodingUtils.encodeLongitude(5); + int cy = GeoEncodingUtils.encodeLatitude(4); + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minX = GeoEncodingUtils.encodeLongitude(4); + int maxX = GeoEncodingUtils.encodeLatitude(5); + int minY = GeoEncodingUtils.encodeLongitude(4); + int maxY = GeoEncodingUtils.encodeLatitude(5); + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relate(minX, maxX, minY, maxY)); + } + + public void testTriangleIntersects() { + Rectangle rectangle = new Rectangle(0, 1, 0, 1); + Component2D component = LatLonComponent2DFactory.create(rectangle); + int ax = GeoEncodingUtils.encodeLongitude(0.5); + int ay = GeoEncodingUtils.encodeLatitude(0.5); + int bx = GeoEncodingUtils.encodeLongitude(2); + int by = GeoEncodingUtils.encodeLatitude(2); + int cx = GeoEncodingUtils.encodeLongitude(0.5); + int cy = GeoEncodingUtils.encodeLatitude(2); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minX = GeoEncodingUtils.encodeLongitude(0.5); + int maxX = GeoEncodingUtils.encodeLatitude(2); + int minY = GeoEncodingUtils.encodeLongitude(0.5); + int maxY = GeoEncodingUtils.encodeLatitude(2); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relate(minX, maxX, minY, maxY)); + } + + public void testTriangleWithin() { + Rectangle rectangle = new Rectangle(0, 1, 0, 1); + Component2D component = LatLonComponent2DFactory.create(rectangle); + int ax = GeoEncodingUtils.encodeLongitude(0.25); + int ay = GeoEncodingUtils.encodeLatitude(0.25); + int bx = GeoEncodingUtils.encodeLongitude(0.5); + int by = GeoEncodingUtils.encodeLatitude(0.5); + int cx = GeoEncodingUtils.encodeLongitude(0.5); + int cy = GeoEncodingUtils.encodeLatitude(0.25); + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minX = GeoEncodingUtils.encodeLongitude(0.25); + int maxX = GeoEncodingUtils.encodeLatitude(0.5); + int minY = GeoEncodingUtils.encodeLongitude(0.25); + int maxY = GeoEncodingUtils.encodeLatitude(0.5); + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relate(minX, maxX, minY, maxY)); + } + + public void testTriangleContains() { + Rectangle rectangle = new Rectangle(0, 1, 0, 1); + Component2D component = LatLonComponent2DFactory.create(rectangle); + int ax = GeoEncodingUtils.encodeLongitude(-60.); + int ay = GeoEncodingUtils.encodeLatitude(-1); + int bx = GeoEncodingUtils.encodeLongitude(2); + int by = GeoEncodingUtils.encodeLatitude(-1); + int cx = GeoEncodingUtils.encodeLongitude(2); + int cy = GeoEncodingUtils.encodeLatitude(60); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minX = GeoEncodingUtils.encodeLongitude(-1); + int maxX = GeoEncodingUtils.encodeLatitude(2); + int minY = GeoEncodingUtils.encodeLongitude(-1); + int maxY = GeoEncodingUtils.encodeLatitude(2); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relate(minX, maxX, minY, maxY)); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYComponent2DTree.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYComponent2DTree.java new file mode 100644 index 000000000000..bb4c38dcf528 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYComponent2DTree.java @@ -0,0 +1,87 @@ +/* + * 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.component2D; + +import org.apache.lucene.document.BaseXYShapeTestCase; +import org.apache.lucene.geo.ShapeTestUtil; +import org.apache.lucene.index.PointValues; +import org.apache.lucene.util.TestUtil; + +public class TestXYComponent2DTree extends TestBaseXYComponent2D { + + @Override + protected Object nextShape() { + int numComponents = TestUtil.nextInt(random(), 2, 10); + Object[] components = new Object[numComponents]; + for (int i =0; i < numComponents; i++) { + components[i] = createRandomShape(); + } + return components; + } + + @Override + protected Component2D getComponent(Object shape) { + return XYComponent2DFactory.create((Object[]) shape); + } + + private Object createRandomShape() { + int type = random().nextInt(4); + switch (type) { + case 0 : return new double[] {ShapeTestUtil.nextDouble(), ShapeTestUtil.nextDouble()}; + case 1 : return ShapeTestUtil.nextBox(); + case 2 : return ShapeTestUtil.nextPolygon(); + case 3 : return BaseXYShapeTestCase.getNextLine(); + default: throw new IllegalArgumentException("Unreachable code"); + } + } + + // because currently shapes can overlap, we need different logic here + @Override + public void testRandomTriangles() { + Object rectangle = nextShape(); + Component2D component = getComponent(rectangle); + + for (int i =0; i < 100; i++) { + int ax = nextEncodedX(); + int ay = nextEncodedY(); + int bx = nextEncodedX(); + int by = nextEncodedY(); + int cx = nextEncodedX(); + int cy = nextEncodedY(); + + int tMinX = StrictMath.min(StrictMath.min(ax, bx), cx); + int tMaxX = StrictMath.max(StrictMath.max(ax, bx), cx); + int tMinY = StrictMath.min(StrictMath.min(ay, by), cy); + int tMaxY = StrictMath.max(StrictMath.max(ay, by), cy); + + PointValues.Relation r = component.relate(tMinX, tMaxX, tMinY, tMaxY); + if (r == PointValues.Relation.CELL_OUTSIDE_QUERY) { + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relateTriangle(ax, ay, bx, by, cx, cy)); + } + if (component.contains(ax, ay)) { + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(ax, ay, ax, ay, ax, ay)); + } + if (component.contains(bx, by)) { + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(bx, by, bx, by, bx, by)); + } + if (component.contains(cx, cy)) { + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(cx, cy, cx, cy, cx, cy)); + } + } + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYLineComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYLineComponent2D.java new file mode 100644 index 000000000000..61883ea8905a --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYLineComponent2D.java @@ -0,0 +1,40 @@ +/* + * 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.component2D; + +import org.apache.lucene.document.TestXYLineShapeQueries;; +import org.apache.lucene.geo.XYLine; + + +public class TestXYLineComponent2D extends TestBaseXYComponent2D { + + @Override + protected Object nextShape() { + return TestXYLineShapeQueries.getNextLine(); + } + + @Override + protected Component2D getComponent(Object shape) { + if (random().nextBoolean()) { + return XYComponent2DFactory.create(shape); + } else { + return XYComponent2DFactory.create((XYLine) shape); + } + } + +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYPointComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYPointComponent2D.java new file mode 100644 index 000000000000..c2d684511530 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYPointComponent2D.java @@ -0,0 +1,107 @@ +/* + * 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.component2D; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.ShapeTestUtil; +import org.apache.lucene.index.PointValues; + +public class TestXYPointComponent2D extends TestBaseXYComponent2D { + + @Override + protected Object nextShape() { + return new double[] {ShapeTestUtil.nextDouble(), ShapeTestUtil.nextDouble()}; + } + + @Override + protected Component2D getComponent(Object shape) { + if (random().nextBoolean()) { + return XYComponent2DFactory.create(shape); + } else { + return XYComponent2DFactory.create((double[]) shape); + } + } + + public void testTriangleDisjoint() { + double[] point = new double[]{0, 1}; + Component2D component = LatLonComponent2DFactory.create(point); + int ax = GeoEncodingUtils.encodeLongitude(4); + int ay = GeoEncodingUtils.encodeLatitude(4); + int bx = GeoEncodingUtils.encodeLongitude(5); + int by = GeoEncodingUtils.encodeLatitude(5); + int cx = GeoEncodingUtils.encodeLongitude(5); + int cy = GeoEncodingUtils.encodeLatitude(4); + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minLat = GeoEncodingUtils.encodeLongitude(4); + int maxLat = GeoEncodingUtils.encodeLatitude(5); + int minLon = GeoEncodingUtils.encodeLongitude(4); + int maxLon = GeoEncodingUtils.encodeLatitude(5); + assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, component.relate(minLat, maxLat, minLon, maxLon)); + } + + public void testTriangleIntersects() { + double[] point = new double[]{0.5, 0.5}; + Component2D component = LatLonComponent2DFactory.create(point); + int ax = GeoEncodingUtils.encodeLongitude(0.5); + int ay = GeoEncodingUtils.encodeLatitude(0.5); + int bx = GeoEncodingUtils.encodeLongitude(2); + int by = GeoEncodingUtils.encodeLatitude(2); + int cx = GeoEncodingUtils.encodeLongitude(0.5); + int cy = GeoEncodingUtils.encodeLatitude(2); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minLat = GeoEncodingUtils.encodeLongitude(0.5); + int maxLat = GeoEncodingUtils.encodeLatitude(2); + int minLon = GeoEncodingUtils.encodeLongitude(0.5); + int maxLon = GeoEncodingUtils.encodeLatitude(2); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relate(minLat, maxLat, minLon, maxLon)); + } + + public void testTriangleWithin() { + double[] point = new double[]{0.5, 0.5}; + Component2D component = LatLonComponent2DFactory.create(point); + int ax = GeoEncodingUtils.encodeLongitude(0.5); + int ay = GeoEncodingUtils.encodeLatitude(0.5); + int bx = GeoEncodingUtils.encodeLongitude(0.5); + int by = GeoEncodingUtils.encodeLatitude(0.5); + int cx = GeoEncodingUtils.encodeLongitude(0.5); + int cy = GeoEncodingUtils.encodeLatitude(0.5); + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minX = GeoEncodingUtils.encodeLongitude(0.5); + int maxX = GeoEncodingUtils.encodeLongitude(0.5); + int minY = GeoEncodingUtils.encodeLatitude(0.5); + int maxY = GeoEncodingUtils.encodeLatitude(0.5); + assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, component.relate(minX, maxX, minY, maxY)); + } + + public void testTriangleContains() { + double[] point = new double[]{0.5, 0.5}; + Component2D component = LatLonComponent2DFactory.create(point); + int ax = GeoEncodingUtils.encodeLongitude(-60.); + int ay = GeoEncodingUtils.encodeLatitude(-1); + int bx = GeoEncodingUtils.encodeLongitude(2); + int by = GeoEncodingUtils.encodeLatitude(-1); + int cx = GeoEncodingUtils.encodeLongitude(2); + int cy = GeoEncodingUtils.encodeLatitude(60); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relateTriangle(ax, ay, bx, by , cx, cy)); + int minX = GeoEncodingUtils.encodeLongitude(-1); + int maxX = GeoEncodingUtils.encodeLatitude(2); + int minY = GeoEncodingUtils.encodeLongitude(-1); + int maxY = GeoEncodingUtils.encodeLatitude(2); + assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, component.relate(minX, maxX, minY, maxY)); + } +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYPolygonComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYPolygonComponent2D.java new file mode 100644 index 000000000000..4e0d09a746c7 --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYPolygonComponent2D.java @@ -0,0 +1,39 @@ +/* + * 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.component2D; + +import org.apache.lucene.geo.ShapeTestUtil; +import org.apache.lucene.geo.XYPolygon; + +public class TestXYPolygonComponent2D extends TestBaseXYComponent2D { + + @Override + protected Object nextShape() { + return ShapeTestUtil.nextPolygon(); + } + + @Override + protected Component2D getComponent(Object shape) { + if (random().nextBoolean()) { + return XYComponent2DFactory.create(shape); + } else { + return XYComponent2DFactory.create((XYPolygon) shape); + } + } + +} diff --git a/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYRectangleComponent2D.java b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYRectangleComponent2D.java new file mode 100644 index 000000000000..562a75160cce --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/component2D/TestXYRectangleComponent2D.java @@ -0,0 +1,39 @@ +/* + * 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.component2D; + +import org.apache.lucene.geo.ShapeTestUtil; +import org.apache.lucene.geo.XYRectangle; + +public class TestXYRectangleComponent2D extends TestBaseXYComponent2D { + + @Override + protected Object nextShape() { + return ShapeTestUtil.nextBox(); + } + + @Override + protected Component2D getComponent(Object shape) { + if (random().nextBoolean()) { + return XYComponent2DFactory.create(shape); + } else { + return XYComponent2DFactory.create((XYRectangle) shape); + } + } + +}