diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index acf0deeffe1..3461e57e455 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -241,6 +241,9 @@ New Features https://github.com/snowballstem/snowball/blob/master/algorithms/arabic.sbl (Ryadh Dahimene via Jim Ferenczi) +* LUCENE-8554: Add new LatLonShapeLineQuery that queries indexed LatLonShape fields + by arbitrary lines. (Nick Knize) + Improvements: * LUCENE-8521: Change LatLonShape encoding to 7 dimensions instead of 6; where the diff --git a/lucene/core/src/java/org/apache/lucene/geo/EdgeTree.java b/lucene/core/src/java/org/apache/lucene/geo/EdgeTree.java new file mode 100644 index 00000000000..d954f88317f --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/geo/EdgeTree.java @@ -0,0 +1,426 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.geo; + +import java.util.Arrays; +import java.util.Comparator; + +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.ArrayUtil; + +import static org.apache.lucene.geo.GeoUtils.lineCrossesLine; +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. + * {@link #relate relate()} are {@code O(n)}, but for most + * practical lines and polygons are much faster than brute force. + * @lucene.internal + */ +public abstract class EdgeTree { + /** minimum latitude of this geometry's bounding box area */ + public final double minLat; + /** maximum latitude of this geometry's bounding box area */ + public final double maxLat; + /** minimum longitude of this geometry's bounding box area */ + public final double minLon; + /** maximum longitude of this geometry's bounding box area */ + public final double maxLon; + + // each component is a node in an augmented 2d kd-tree: we alternate splitting between latitude/longitude, + // and pull up max values for both dimensions to each parent node (regardless of split). + + /** maximum latitude of this component or any of its children */ + protected double maxY; + /** maximum longitude of this component or any of its children */ + protected double maxX; + /** which dimension was this node split on */ + // TODO: its implicit based on level, but boolean keeps code simple + protected boolean splitX; + + // child components, or null + protected EdgeTree left; + protected EdgeTree right; + + /** root node of edge tree */ + protected final Edge tree; + + protected EdgeTree(final double minLat, final double maxLat, final double minLon, final double maxLon, double[] lats, double[] lons) { + this.minLat = minLat; + this.maxLat = maxLat; + this.minLon = minLon; + this.maxLon = maxLon; + this.maxY = maxLat; + this.maxX = maxLon; + + // create interval tree of edges + this.tree = createTree(lats, lons); + } + + /** Returns relation to the provided triangle */ + public Relation relateTriangle(double ax, double ay, double bx, double by, double cx, double cy) { + // compute bounding box of triangle + double minLat = StrictMath.min(StrictMath.min(ay, by), cy); + double minLon = StrictMath.min(StrictMath.min(ax, bx), cx); + double maxLat = StrictMath.max(StrictMath.max(ay, by), cy); + double maxLon = StrictMath.max(StrictMath.max(ax, bx), cx); + if (minLat <= maxY && minLon <= maxX) { + Relation relation = internalComponentRelateTriangle(ax, ay, bx, by, cx, cy); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + if (left != null) { + relation = left.relateTriangle(ax, ay, bx, by, cx, cy); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && maxLon >= this.minLon))) { + relation = right.relateTriangle(ax, ay, bx, by, cx, cy); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + } + return Relation.CELL_OUTSIDE_QUERY; + } + + /** Returns relation to the provided rectangle */ + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + if (minLat <= maxY && minLon <= maxX) { + Relation relation = internalComponentRelate(minLat, maxLat, minLon, maxLon); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + if (left != null) { + relation = left.relate(minLat, maxLat, minLon, maxLon); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && maxLon >= this.minLon))) { + relation = right.relate(minLat, maxLat, minLon, maxLon); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + } + return Relation.CELL_OUTSIDE_QUERY; + } + + protected Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) { + return null; + } + protected Relation componentRelateTriangle(double ax, double ay, double bx, double by, double cx, double cy) { + return null; + } + + private Relation internalComponentRelateTriangle(double ax, double ay, double bx, double by, double cx, double cy) { + // compute bounding box of triangle + double minLat = StrictMath.min(StrictMath.min(ay, by), cy); + double minLon = StrictMath.min(StrictMath.min(ax, bx), cx); + double maxLat = StrictMath.max(StrictMath.max(ay, by), cy); + double maxLon = StrictMath.max(StrictMath.max(ax, bx), cx); + if (maxLon < this.minLon || minLon > this.maxLon || maxLat < this.minLat || minLat > this.maxLat) { + return Relation.CELL_OUTSIDE_QUERY; + } + + Relation shapeRelation = componentRelateTriangle(ax, ay, bx, by, cx, cy); + if (shapeRelation != null) { + return shapeRelation; + } + + // we cross + if (tree.crossesTriangle(ax, ay, bx, by, cx, cy)) { + return Relation.CELL_CROSSES_QUERY; + } + return Relation.CELL_OUTSIDE_QUERY; + } + + + /** Returns relation to the provided rectangle for this component */ + protected Relation internalComponentRelate(double minLat, double maxLat, double minLon, double maxLon) { + // if the bounding boxes are disjoint then the shape does not cross + if (maxLon < this.minLon || minLon > this.maxLon || maxLat < this.minLat || minLat > this.maxLat) { + return Relation.CELL_OUTSIDE_QUERY; + } + // if the rectangle fully encloses us, we cross. + if (minLat <= this.minLat && maxLat >= this.maxLat && minLon <= this.minLon && maxLon >= this.maxLon) { + return Relation.CELL_CROSSES_QUERY; + } + + Relation shapeRelation = componentRelate(minLat, maxLat, minLon, maxLon); + if (shapeRelation != null) { + return shapeRelation; + } + + // we cross + if (tree.crosses(minLat, maxLat, minLon, maxLon)) { + return Relation.CELL_CROSSES_QUERY; + } + + return Relation.CELL_OUTSIDE_QUERY; + } + + /** Creates tree from sorted components (with range low and high inclusive) */ + protected static EdgeTree createTree(EdgeTree 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 = Double.compare(left.minLon, right.minLon); + if (ret == 0) { + ret = Double.compare(left.maxX, right.maxX); + } + return ret; + }; + } else { + comparator = (left, right) -> { + int ret = Double.compare(left.minLat, right.minLat); + if (ret == 0) { + ret = Double.compare(left.maxY, right.maxY); + } + return ret; + }; + } + ArrayUtil.select(components, low, high + 1, mid, comparator); + } + // add midpoint + EdgeTree newNode = components[mid]; + newNode.splitX = splitX; + // add children + newNode.left = createTree(components, low, mid - 1, !splitX); + newNode.right = createTree(components, mid + 1, high, !splitX); + // pull up max values to this node + if (newNode.left != null) { + newNode.maxX = Math.max(newNode.maxX, newNode.left.maxX); + newNode.maxY = Math.max(newNode.maxY, newNode.left.maxY); + } + if (newNode.right != null) { + newNode.maxX = Math.max(newNode.maxX, newNode.right.maxX); + newNode.maxY = Math.max(newNode.maxY, newNode.right.maxY); + } + return newNode; + } + + /** + * Internal tree node: represents geometry edge from lat1,lon1 to lat2,lon2. + * The sort value is {@code low}, which is the minimum latitude of the edge. + * {@code max} stores the maximum latitude of this edge or any children. + */ + static class Edge { + // lat-lon pair (in original order) of the two vertices + final double lat1, lat2; + final double lon1, lon2; + /** min of this edge */ + final double low; + /** max latitude of this edge or any children */ + double max; + + /** left child edge, or null */ + Edge left; + /** right child edge, or null */ + Edge right; + + Edge(double lat1, double lon1, double lat2, double lon2, double low, double max) { + this.lat1 = lat1; + this.lon1 = lon1; + this.lat2 = lat2; + this.lon2 = lon2; + this.low = low; + this.max = max; + } + + /** Returns true if the triangle crosses any edge in this edge subtree */ + boolean crossesTriangle(double ax, double ay, double bx, double by, double cx, double cy) { + // compute bounding box of triangle + double minLat = StrictMath.min(StrictMath.min(ay, by), cy); + double minLon = StrictMath.min(StrictMath.min(ax, bx), cx); + double maxLat = StrictMath.max(StrictMath.max(ay, by), cy); + double maxLon = StrictMath.max(StrictMath.max(ax, bx), cx); + + if (minLat <= max) { + double dy = lat1; + double ey = lat2; + double dx = lon1; + double ex = lon2; + + // 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 < minLat && ey < minLat) || + (dy > maxLat && ey > maxLat) || + (dx < minLon && ex < minLon) || + (dx > maxLon && ex > maxLon); + + if (outside == false) { + // does triangle's first edge intersect polyline? + // ax, ay -> bx, by + if (lineCrossesLine(ax, ay, bx, by, dx, dy, ex, ey)) { + return true; + } + + // does triangle's second edge intersect polyline? + // bx, by -> cx, cy + if (lineCrossesLine(bx, by, cx, cy, dx, dy, ex, ey)) { + return true; + } + + // does triangle's third edge intersect polyline? + // cx, cy -> ax, ay + if (lineCrossesLine(cx, cy, ax, ay, dx, dy, ex, ey)) { + return true; + } + } + + if (left != null) { + if (left.crossesTriangle(ax, ay, bx, by, cx, cy)) { + return true; + } + } + + if (right != null && maxLat >= low) { + if (right.crossesTriangle(ax, ay, bx, by, cx, cy)) { + return true; + } + } + } + return false; + } + + /** Returns true if the box crosses any edge in this edge subtree */ + boolean crosses(double minLat, double maxLat, double minLon, double maxLon) { + // we just have to cross one edge to answer the question, so we descend the tree and return when we do. + if (minLat <= 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 cy = lat1; + double dy = lat2; + double cx = lon1; + double dx = lon2; + + // 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 = (cy < minLat && dy < minLat) || + (cy > maxLat && dy > maxLat) || + (cx < minLon && dx < minLon) || + (cx > maxLon && dx > maxLon); + // optimization: see if either end of the line segment is contained by the rectangle + if (Rectangle.containsPoint(cy, cx, minLat, maxLat, minLon, maxLon) + || Rectangle.containsPoint(dy, dx, minLat, maxLat, minLon, maxLon)) { + return true; + } + + if (outside == false) { + // does box's top edge intersect polyline? + // ax = minLon, bx = maxLon, ay = maxLat, by = maxLat + if (orient(cx, cy, dx, dy, minLon, maxLat) * orient(cx, cy, dx, dy, maxLon, maxLat) <= 0 && + orient(minLon, maxLat, maxLon, maxLat, cx, cy) * orient(minLon, maxLat, maxLon, maxLat, dx, dy) <= 0) { + return true; + } + + // does box's right edge intersect polyline? + // ax = maxLon, bx = maxLon, ay = maxLat, by = minLat + if (orient(cx, cy, dx, dy, maxLon, maxLat) * orient(cx, cy, dx, dy, maxLon, minLat) <= 0 && + orient(maxLon, maxLat, maxLon, minLat, cx, cy) * orient(maxLon, maxLat, maxLon, minLat, dx, dy) <= 0) { + return true; + } + + // does box's bottom edge intersect polyline? + // ax = maxLon, bx = minLon, ay = minLat, by = minLat + if (orient(cx, cy, dx, dy, maxLon, minLat) * orient(cx, cy, dx, dy, minLon, minLat) <= 0 && + orient(maxLon, minLat, minLon, minLat, cx, cy) * orient(maxLon, minLat, minLon, minLat, dx, dy) <= 0) { + return true; + } + + // does box's left edge intersect polyline? + // ax = minLon, bx = minLon, ay = minLat, by = maxLat + if (orient(cx, cy, dx, dy, minLon, minLat) * orient(cx, cy, dx, dy, minLon, maxLat) <= 0 && + orient(minLon, minLat, minLon, maxLat, cx, cy) * orient(minLon, minLat, minLon, maxLat, dx, dy) <= 0) { + return true; + } + } + + if (left != null) { + if (left.crosses(minLat, maxLat, minLon, maxLon)) { + return true; + } + } + + if (right != null && maxLat >= low) { + if (right.crosses(minLat, maxLat, minLon, maxLon)) { + return true; + } + } + } + return false; + } + } + + /** + * Creates an edge interval tree from a set of geometry vertices. + * @return root node of the tree. + */ + private static Edge createTree(double[] lats, double[] lons) { + Edge edges[] = new Edge[lats.length - 1]; + for (int i = 1; i < lats.length; i++) { + double lat1 = lats[i-1]; + double lon1 = lons[i-1]; + double lat2 = lats[i]; + double lon2 = lons[i]; + edges[i - 1] = new Edge(lat1, lon1, lat2, lon2, Math.min(lat1, lat2), Math.max(lat1, lat2)); + } + // 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 Edge createTree(Edge edges[], int low, int high) { + if (low > high) { + return null; + } + // add midpoint + int mid = (low + high) >>> 1; + Edge 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; + } +} diff --git a/lucene/core/src/java/org/apache/lucene/geo/GeoUtils.java b/lucene/core/src/java/org/apache/lucene/geo/GeoUtils.java index 468de938381..0c730321439 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/GeoUtils.java +++ b/lucene/core/src/java/org/apache/lucene/geo/GeoUtils.java @@ -194,6 +194,20 @@ public final class GeoUtils { } } + /** uses orient method to compute whether two line segments cross */ + public static boolean lineCrossesLine(double a1x, double a1y, double b1x, double b1y, double a2x, double a2y, double b2x, double b2y) { + // shortcut: either "line" is actually a point + if ((a1x == b1x && a1y == b1y) || (a2x == b2x && a2y == b2y)) { + return false; + } + + if (orient(a2x, a2y, b2x, b2y, a1x, a1y) * orient(a2x, a2y, b2x, b2y, b1x, b1y) <= 0 && + orient(a1x, a1y, b1x, b1y, a2x, a2y) * orient(a1x, a1y, b1x, b1y, b2x, b2y) <= 0) { + return true; + } + return false; + } + /** * used to define the orientation of 3 points * -1 = Clockwise diff --git a/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java b/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java index 64a37846607..fee23d0ed4c 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java @@ -16,72 +16,29 @@ */ package org.apache.lucene.geo; -import java.util.Arrays; -import java.util.Comparator; - import org.apache.lucene.index.PointValues.Relation; -import org.apache.lucene.util.ArrayUtil; - -import static org.apache.lucene.geo.GeoUtils.orient; /** * 2D polygon implementation represented as a balanced interval tree of edges. *

- * Construction takes {@code O(n log n)} time for sorting and tree construction. - * {@link #contains contains()} and {@link #relate relate()} are {@code O(n)}, but for most - * practical polygons are much faster than brute force. - *

* Loosely based on the algorithm described in * http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf. * @lucene.internal */ // Both Polygon.contains() and Polygon.crossesSlowly() loop all edges, and first check that the edge is within a range. -// we just organize the edges to do the same computations on the same subset of edges more efficiently. -public final class Polygon2D { - /** minimum latitude of this polygon's bounding box area */ - public final double minLat; - /** maximum latitude of this polygon's bounding box area */ - public final double maxLat; - /** minimum longitude of this polygon's bounding box area */ - public final double minLon; - /** maximum longitude of this polygon's bounding box area */ - public final double maxLon; - +// we just organize the edges to do the same computations on the same subset of edges more efficiently. +public final class Polygon2D extends EdgeTree { // each component/hole is a node in an augmented 2d kd-tree: we alternate splitting between latitude/longitude, // and pull up max values for both dimensions to each parent node (regardless of split). - - /** maximum latitude of this component or any of its children */ - private double maxY; - /** maximum longitude of this component or any of its children */ - private double maxX; - /** which dimension was this node split on */ - // TODO: its implicit based on level, but boolean keeps code simple - private boolean splitX; - - // child components, or null - private Polygon2D left; - private Polygon2D right; - /** tree of holes, or null */ private final Polygon2D holes; - - /** root node of edge tree */ - private final Edge tree; private Polygon2D(Polygon polygon, Polygon2D holes) { + super(polygon.minLat, polygon.maxLat, polygon.minLon, polygon.maxLon, polygon.getPolyLats(), polygon.getPolyLons()); this.holes = holes; - this.minLat = polygon.minLat; - this.maxLat = polygon.maxLat; - this.minLon = polygon.minLon; - this.maxLon = polygon.maxLon; - this.maxY = maxLat; - this.maxX = maxLon; - - // create interval tree of edges - this.tree = createTree(polygon.getPolyLats(), polygon.getPolyLons()); } - /** + /** * Returns true if the point is contained within this polygon. *

* See @@ -93,96 +50,36 @@ public final class Polygon2D { return true; } if (left != null) { - if (left.contains(latitude, longitude)) { + if (((Polygon2D)left).contains(latitude, longitude)) { return true; } } if (right != null && ((splitX == false && latitude >= minLat) || (splitX && longitude >= minLon))) { - if (right.contains(latitude, longitude)) { + if (((Polygon2D)right).contains(latitude, longitude)) { return true; } } } return false; } - + /** Returns true if the point is contained within this polygon component. */ private boolean componentContains(double latitude, double longitude) { // check bounding box if (latitude < minLat || latitude > maxLat || longitude < minLon || longitude > maxLon) { return false; } - - if (tree.contains(latitude, longitude)) { + if (contains(tree, latitude, longitude)) { if (holes != null && holes.contains(latitude, longitude)) { return false; } return true; } - return false; } - /** Returns relation to the provided triangle */ - public Relation relateTriangle(double ax, double ay, double bx, double by, double cx, double cy) { - // compute bounding box of triangle - double minLat = StrictMath.min(StrictMath.min(ay, by), cy); - double minLon = StrictMath.min(StrictMath.min(ax, bx), cx); - double maxLat = StrictMath.max(StrictMath.max(ay, by), cy); - double maxLon = StrictMath.max(StrictMath.max(ax, bx), cx); - if (minLat <= maxY && minLon <= maxX) { - Relation relation = componentRelateTriangle(ax, ay, bx, by, cx, cy); - if (relation != Relation.CELL_OUTSIDE_QUERY) { - return relation; - } - if (left != null) { - relation = left.relateTriangle(ax, ay, bx, by, cx, cy); - if (relation != Relation.CELL_OUTSIDE_QUERY) { - return relation; - } - } - if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && maxLon >= this.minLon))) { - relation = right.relateTriangle(ax, ay, bx, by, cx, cy); - if (relation != Relation.CELL_OUTSIDE_QUERY) { - return relation; - } - } - } - return Relation.CELL_OUTSIDE_QUERY; - } - - /** Returns relation to the provided rectangle */ - public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { - if (minLat <= maxY && minLon <= maxX) { - Relation relation = componentRelate(minLat, maxLat, minLon, maxLon); - if (relation != Relation.CELL_OUTSIDE_QUERY) { - return relation; - } - if (left != null) { - relation = left.relate(minLat, maxLat, minLon, maxLon); - if (relation != Relation.CELL_OUTSIDE_QUERY) { - return relation; - } - } - if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && maxLon >= this.minLon))) { - relation = right.relate(minLat, maxLat, minLon, maxLon); - if (relation != Relation.CELL_OUTSIDE_QUERY) { - return relation; - } - } - } - return Relation.CELL_OUTSIDE_QUERY; - } - - private Relation componentRelateTriangle(double ax, double ay, double bx, double by, double cx, double cy) { - // compute bounding box of triangle - double minLat = StrictMath.min(StrictMath.min(ay, by), cy); - double minLon = StrictMath.min(StrictMath.min(ax, bx), cx); - double maxLat = StrictMath.max(StrictMath.max(ay, by), cy); - double maxLon = StrictMath.max(StrictMath.max(ax, bx), cx); - if (maxLon < this.minLon || minLon > this.maxLon || maxLat < this.minLat || minLat > this.maxLat) { - return Relation.CELL_OUTSIDE_QUERY; - } + @Override + protected Relation componentRelateTriangle(double ax, double ay, double bx, double by, double cx, double cy) { // check any holes if (holes != null) { Relation holeRelation = holes.relateTriangle(ax, ay, bx, by, cx, cy); @@ -202,24 +99,12 @@ public final class Polygon2D { } else if (numCorners > 0) { return Relation.CELL_CROSSES_QUERY; } - - // we cross - if (tree.crossesTriangle(ax, ay, bx, by, cx, cy)) { - return Relation.CELL_CROSSES_QUERY; - } - return Relation.CELL_OUTSIDE_QUERY; + return null; } /** Returns relation to the provided rectangle for this component */ - private Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) { - // if the bounding boxes are disjoint then the shape does not cross - if (maxLon < this.minLon || minLon > this.maxLon || maxLat < this.minLat || minLat > this.maxLat) { - return Relation.CELL_OUTSIDE_QUERY; - } - // if the rectangle fully encloses us, we cross. - if (minLat <= this.minLat && maxLat >= this.maxLat && minLon <= this.minLon && maxLon >= this.maxLon) { - return Relation.CELL_CROSSES_QUERY; - } + @Override + protected Relation componentRelate(double minLat, double maxLat, double minLon, double maxLon) { // check any holes if (holes != null) { Relation holeRelation = holes.relate(minLat, maxLat, minLon, maxLon); @@ -239,13 +124,7 @@ public final class Polygon2D { } else if (numCorners > 0) { return Relation.CELL_CROSSES_QUERY; } - - // we cross - if (tree.crosses(minLat, maxLat, minLon, maxLon)) { - return Relation.CELL_CROSSES_QUERY; - } - - return Relation.CELL_OUTSIDE_QUERY; + return null; } private int numberOfTriangleCorners(double ax, double ay, double bx, double by, double cx, double cy) { @@ -288,52 +167,7 @@ public final class Polygon2D { } return containsCount; } - - /** Creates tree from sorted components (with range low and high inclusive) */ - private static Polygon2D createTree(Polygon2D 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 = Double.compare(left.minLon, right.minLon); - if (ret == 0) { - ret = Double.compare(left.maxX, right.maxX); - } - return ret; - }; - } else { - comparator = (left, right) -> { - int ret = Double.compare(left.minLat, right.minLat); - if (ret == 0) { - ret = Double.compare(left.maxY, right.maxY); - } - return ret; - }; - } - ArrayUtil.select(components, low, high + 1, mid, comparator); - } - // add midpoint - Polygon2D newNode = components[mid]; - newNode.splitX = splitX; - // add children - newNode.left = createTree(components, low, mid - 1, !splitX); - newNode.right = createTree(components, mid + 1, high, !splitX); - // pull up max values to this node - if (newNode.left != null) { - newNode.maxX = Math.max(newNode.maxX, newNode.left.maxX); - newNode.maxY = Math.max(newNode.maxY, newNode.left.maxY); - } - if (newNode.right != null) { - newNode.maxX = Math.max(newNode.maxX, newNode.right.maxX); - newNode.maxY = Math.max(newNode.maxY, newNode.right.maxY); - } - return newNode; - } - + /** Builds a Polygon2D from multipolygon */ public static Polygon2D create(Polygon... polygons) { Polygon2D components[] = new Polygon2D[polygons.length]; @@ -346,253 +180,55 @@ public final class Polygon2D { } components[i] = new Polygon2D(gon, holes); } - return createTree(components, 0, components.length - 1, false); + return (Polygon2D)createTree(components, 0, components.length - 1, false); } - /** - * Internal tree node: represents polygon edge from lat1,lon1 to lat2,lon2. - * The sort value is {@code low}, which is the minimum latitude of the edge. - * {@code max} stores the maximum latitude of this edge or any children. + /** + * 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. */ - static final class Edge { - // lat-lon pair (in original order) of the two vertices - final double lat1, lat2; - final double lon1, lon2; - /** min of this edge */ - final double low; - /** max latitude of this edge or any children */ - double max; - - /** left child edge, or null */ - Edge left; - /** right child edge, or null */ - Edge right; - - Edge(double lat1, double lon1, double lat2, double lon2, double low, double max) { - this.lat1 = lat1; - this.lon1 = lon1; - this.lat2 = lat2; - this.lon2 = lon2; - 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. - boolean contains(double latitude, double longitude) { - // crossings algorithm is an odd-even algorithm, so we descend the tree xor'ing results along our path - boolean res = false; - if (latitude <= max) { - if (lat1 > latitude != lat2 > latitude) { - if (longitude < (lon1 - lon2) * (latitude - lat2) / (lat1 - lat2) + lon2) { - res = true; - } - } - if (left != null) { - res ^= left.contains(latitude, longitude); - } - if (right != null && latitude >= low) { - res ^= right.contains(latitude, longitude); + // 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. + private static boolean contains(Edge tree, double latitude, double longitude) { + // crossings algorithm is an odd-even algorithm, so we descend the tree xor'ing results along our path + boolean res = false; + if (latitude <= tree.max) { + if (tree.lat1 > latitude != tree.lat2 > latitude) { + if (longitude < (tree.lon1 - tree.lon2) * (latitude - tree.lat2) / (tree.lat1 - tree.lat2) + tree.lon2) { + res = true; } } - return res; - } - - /** Returns true if the triangle crosses any edge in this edge subtree */ - boolean crossesTriangle(double ax, double ay, double bx, double by, double cx, double cy) { - // compute bounding box of triangle - double minLat = StrictMath.min(StrictMath.min(ay, by), cy); - double minLon = StrictMath.min(StrictMath.min(ax, bx), cx); - double maxLat = StrictMath.max(StrictMath.max(ay, by), cy); - double maxLon = StrictMath.max(StrictMath.max(ax, bx), cx); - - if (minLat <= max) { - double dy = lat1; - double ey = lat2; - double dx = lon1; - double ex = lon2; - - // 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 < minLat && ey < minLat) || - (dy > maxLat && ey > maxLat) || - (dx < minLon && ex < minLon) || - (dx > maxLon && ex > maxLon); - - if (outside == false) { - // does triangle's first edge intersect polyline? - // ax, ay -> bx, by - if (orient(dx, dy, ex, ey, ax, ay) * orient(dx, dy, ex, ey, bx, by) <= 0 && - orient(ax, ay, bx, by, dx, dy) * orient(ax, ay, bx, by, ex, ey) <= 0) { - return true; - } - - // does triangle's second edge intersect polyline? - // bx, by -> cx, cy - if (orient(dx, dy, ex, ey, bx, by) * orient(dx, dy, ex, ey, cx, cy) <= 0 && - orient(bx, by, cx, cy, dx, dy) * orient(bx, by, cx, cy, ex, ey) <= 0) { - return true; - } - - // does triangle's third edge intersect polyline? - // cx, cy -> ax, ay - if (orient(dx, dy, ex, ey, cx, cy) * orient(dx, dy, ex, ey, ax, ay) <= 0 && - orient(cx, cy, ax, ay, dx, dy) * orient(cx, cy, ax, ay, ex, ey) <= 0) { - return true; - } - } - - if (left != null) { - if (left.crossesTriangle(ax, ay, bx, by, cx, cy)) { - return true; - } - } - - if (right != null && maxLat >= low) { - if (right.crossesTriangle(ax, ay, bx, by, cx, cy)) { - return true; - } - } + if (tree.left != null) { + res ^= contains(tree.left, latitude, longitude); } - return false; - } - - /** Returns true if the box crosses any edge in this edge subtree */ - boolean crosses(double minLat, double maxLat, double minLon, double maxLon) { - // we just have to cross one edge to answer the question, so we descend the tree and return when we do. - if (minLat <= 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 cy = lat1; - double dy = lat2; - double cx = lon1; - double dx = lon2; - - // 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 = (cy < minLat && dy < minLat) || - (cy > maxLat && dy > maxLat) || - (cx < minLon && dx < minLon) || - (cx > maxLon && dx > maxLon); - if (outside == false) { - // does box's top edge intersect polyline? - // ax = minLon, bx = maxLon, ay = maxLat, by = maxLat - if (orient(cx, cy, dx, dy, minLon, maxLat) * orient(cx, cy, dx, dy, maxLon, maxLat) <= 0 && - orient(minLon, maxLat, maxLon, maxLat, cx, cy) * orient(minLon, maxLat, maxLon, maxLat, dx, dy) <= 0) { - return true; - } - - // does box's right edge intersect polyline? - // ax = maxLon, bx = maxLon, ay = maxLat, by = minLat - if (orient(cx, cy, dx, dy, maxLon, maxLat) * orient(cx, cy, dx, dy, maxLon, minLat) <= 0 && - orient(maxLon, maxLat, maxLon, minLat, cx, cy) * orient(maxLon, maxLat, maxLon, minLat, dx, dy) <= 0) { - return true; - } - - // does box's bottom edge intersect polyline? - // ax = maxLon, bx = minLon, ay = minLat, by = minLat - if (orient(cx, cy, dx, dy, maxLon, minLat) * orient(cx, cy, dx, dy, minLon, minLat) <= 0 && - orient(maxLon, minLat, minLon, minLat, cx, cy) * orient(maxLon, minLat, minLon, minLat, dx, dy) <= 0) { - return true; - } - - // does box's left edge intersect polyline? - // ax = minLon, bx = minLon, ay = minLat, by = maxLat - if (orient(cx, cy, dx, dy, minLon, minLat) * orient(cx, cy, dx, dy, minLon, maxLat) <= 0 && - orient(minLon, minLat, minLon, maxLat, cx, cy) * orient(minLon, minLat, minLon, maxLat, dx, dy) <= 0) { - return true; - } - } - - if (left != null) { - if (left.crosses(minLat, maxLat, minLon, maxLon)) { - return true; - } - } - - if (right != null && maxLat >= low) { - if (right.crosses(minLat, maxLat, minLon, maxLon)) { - return true; - } - } + if (tree.right != null && latitude >= tree.low) { + res ^= contains(tree.right, latitude, longitude); } - return false; } - } - - /** - * Creates an edge interval tree from a set of polygon vertices. - * @return root node of the tree. - */ - private static Edge createTree(double polyLats[], double polyLons[]) { - Edge edges[] = new Edge[polyLats.length - 1]; - for (int i = 1; i < polyLats.length; i++) { - double lat1 = polyLats[i-1]; - double lon1 = polyLons[i-1]; - double lat2 = polyLats[i]; - double lon2 = polyLons[i]; - edges[i - 1] = new Edge(lat1, lon1, lat2, lon2, Math.min(lat1, lat2), Math.max(lat1, lat2)); - } - // 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 Edge createTree(Edge edges[], int low, int high) { - if (low > high) { - return null; - } - // add midpoint - int mid = (low + high) >>> 1; - Edge 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; + return res; } } diff --git a/lucene/core/src/java/org/apache/lucene/geo/Rectangle.java b/lucene/core/src/java/org/apache/lucene/geo/Rectangle.java index a8200c6edd3..45d437df1bc 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/Rectangle.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Rectangle.java @@ -87,6 +87,13 @@ public class Rectangle { return maxLon < minLon; } + /** returns true if rectangle (defined by minLat, maxLat, minLon, maxLon) contains the lat lon point */ + public static boolean containsPoint(final double lat, final double lon, + final double minLat, final double maxLat, + final double minLon, final double maxLon) { + return lat >= minLat && lat <= maxLat && lon >= minLon && lon <= maxLon; + } + /** Compute Bounding Box for a circle using WGS-84 parameters */ public static Rectangle fromPointDistance(final double centerLat, final double centerLon, final double radiusMeters) { checkLatitude(centerLat); 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 99d7c08d794..580d574e93d 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java @@ -124,6 +124,13 @@ public class LatLonShape { return new LatLonShapeBoundingBoxQuery(field, queryRelation, minLatitude, maxLatitude, minLongitude, maxLongitude); } + /** create a query to find all polygons that intersect a provided linestring (or array of linestrings) + * note: does not support dateline crossing + **/ + public static Query newLineQuery(String field, QueryRelation queryRelation, Line... lines) { + return new LatLonShapeLineQuery(field, queryRelation, lines); + } + /** create a query to find all polygons that intersect a provided polygon (or array of polygons) * note: does not support dateline crossing **/ diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java index cb8f9a1c608..d1c3e14f9da 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeBoundingBoxQuery.java @@ -18,7 +18,7 @@ package org.apache.lucene.document; import java.util.Arrays; -import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.Rectangle; import org.apache.lucene.geo.Tessellator; import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.util.FutureArrays; @@ -37,7 +37,7 @@ import static org.apache.lucene.geo.GeoUtils.orient; * Finds all previously indexed shapes that intersect the specified bounding box. * *

The field must be indexed using - * {@link org.apache.lucene.document.LatLonShape#createIndexableFields(String, Polygon)} added per document. + * {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document. * * @lucene.experimental **/ @@ -99,25 +99,36 @@ final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery { int cY = (int)(c & 0x00000000FFFFFFFFL); if (queryRelation == LatLonShape.QueryRelation.WITHIN) { - return queryContains(aX, aY) && queryContains(bX, bY) && queryContains(cX, cY); + return bboxContainsTriangle(aX, aY, bX, bY, cX, cY, minX, maxX, minY, maxY); } return queryMatches(aX, aY, bX, bY, cX, cY); } - private boolean queryContains(int x, int y) { + /** static utility method to check if a bounding box contains a point */ + private static boolean bboxContainsPoint(int x, int y, int minX, int maxX, int minY, int maxY) { return (x < minX || x > maxX || y < minY || y > maxY) == false; } - private boolean queryContains(int ax, int ay, int bx, int by, int cx, int cy) { - return queryContains(ax, ay) || queryContains(bx, by) || queryContains(cx, cy); + /** static utility method to check if a bounding box contains a triangle */ + private static boolean bboxContainsTriangle(int ax, int ay, int bx, int by, int cx, int cy, + int minX, int maxX, int minY, int maxY) { + return bboxContainsPoint(ax, ay, minX, maxX, minY, maxY) + && bboxContainsPoint(bx, by, minX, maxX, minY, maxY) + && bboxContainsPoint(cx, cy, minX, maxX, minY, maxY); + } + + /** instance method to check if query box contains point */ + private boolean queryContainsPoint(int x, int y) { + return bboxContainsPoint(x, y, this.minX, this.maxX, this.minY, this.maxY); } protected boolean queryMatches(int aX, int aY, int bX, int bY, int cX, int cY) { // 1. query contains any triangle points - if (queryContains(aX, aY, bX, bY, cX, cY)) { + if (queryContainsPoint(aX, aY) || queryContainsPoint(bX, bY) || queryContainsPoint(cX, cY)) { return true; } + // compute bounding box of triangle 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); @@ -139,7 +150,6 @@ final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery { return true; } - // 4. last ditch effort: check crossings if (queryIntersects(aX, aY, bX, bY, cX, cY)) { return true; @@ -148,7 +158,30 @@ final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery { } /** returns true if the edge (defined by (ax, ay) (bx, by)) intersects the query */ - private boolean edgeIntersectsQuery(double ax, double ay, double bx, double by) { + private static boolean edgeIntersectsBox(int ax, int ay, int bx, int by, + int minX, int maxX, int minY, int maxY) { + // shortcut: if edge is a point (occurs w/ Line shapes); simply check bbox w/ point + if (ax == bx && ay == by) { + return Rectangle.containsPoint(ay, ax, minY, maxY, minX, maxX); + } + + // shortcut: check if either of the end points fall inside the box + if (bboxContainsPoint(ax, ay, minX, maxX, minY, maxY) + || bboxContainsPoint(bx, by, minX, maxX, minY, maxY)) { + return true; + } + + // shortcut: check bboxes of edges are disjoint + if (boxesAreDisjoint(Math.min(ax, bx), Math.max(ax, bx), Math.min(ay, by), Math.max(ay, by), + minX, maxX, minY, maxY)) { + return false; + } + + // shortcut: edge is a point + if (ax == bx && ay == by) { + return false; + } + // top if (orient(ax, ay, bx, by, minX, maxY) * orient(ax, ay, bx, by, maxX, maxY) <= 0 && orient(minX, maxY, maxX, maxY, ax, ay) * orient(minX, maxY, maxX, maxY, bx, by) <= 0) { @@ -175,6 +208,11 @@ final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery { return false; } + /** returns true if the edge (defined by (ax, ay) (bx, by)) intersects the query */ + private boolean edgeIntersectsQuery(int ax, int ay, int bx, int by) { + return edgeIntersectsBox(ax, ay, bx, by, this.minX, this.maxX, this.minY, this.maxY); + } + /** returns true if the query intersects the provided triangle (in encoded space) */ private boolean queryIntersects(int ax, int ay, int bx, int by, int cx, int cy) { // check each edge of the triangle against the query @@ -186,6 +224,12 @@ final class LatLonShapeBoundingBoxQuery extends LatLonShapeQuery { return false; } + /** utility method to check if two boxes are disjoint */ + public static boolean boxesAreDisjoint(final int aMinX, final int aMaxX, final int aMinY, final int aMaxY, + final int bMinX, final int bMaxX, final int bMinY, final int bMaxY) { + return (aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY); + } + @Override public boolean equals(Object o) { return sameClassAs(o) && equalsTo(getClass().cast(o)); diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeLineQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeLineQuery.java new file mode 100644 index 00000000000..e49b4ece344 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeLineQuery.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.lucene.document; + +import java.util.Arrays; + +import org.apache.lucene.document.LatLonShape.QueryRelation; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Line2D; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.NumericUtils; + +/** + * Finds all previously indexed shapes that intersect the specified arbitrary {@code Line}. + *

+ * Note: + *

+ *

+ * todo: + *

+ *

The field must be indexed using + * {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document. + * + * @lucene.experimental + **/ +final class LatLonShapeLineQuery extends LatLonShapeQuery { + final Line[] lines; + final private Line2D line2D; + + public LatLonShapeLineQuery(String field, QueryRelation queryRelation, Line... lines) { + super(field, queryRelation); + /** line queries do not support within relations, only intersects and disjoint */ + if (queryRelation == QueryRelation.WITHIN) { + throw new IllegalArgumentException("LatLonShapeLineQuery does not support " + QueryRelation.WITHIN + " queries"); + } + + if (lines == null) { + throw new IllegalArgumentException("lines must not be null"); + } + if (lines.length == 0) { + throw new IllegalArgumentException("lines must not be empty"); + } + for (int i = 0; i < lines.length; ++i) { + if (lines[i] == null) { + throw new IllegalArgumentException("line[" + i + "] must not be null"); + } else if (lines[i].minLon > lines[i].maxLon) { + throw new IllegalArgumentException("LatLonShapeLineQuery does not currently support querying across dateline."); + } + } + this.lines = lines.clone(); + this.line2D = Line2D.create(lines); + } + + @Override + protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle) { + double minLat = GeoEncodingUtils.decodeLatitude(LatLonShape.decodeTriangleBoxVal(minTriangle, minYOffset)); + double minLon = GeoEncodingUtils.decodeLongitude(LatLonShape.decodeTriangleBoxVal(minTriangle, minXOffset)); + double maxLat = GeoEncodingUtils.decodeLatitude(LatLonShape.decodeTriangleBoxVal(maxTriangle, maxYOffset)); + double maxLon = GeoEncodingUtils.decodeLongitude(LatLonShape.decodeTriangleBoxVal(maxTriangle, maxXOffset)); + + // check internal node against query + return line2D.relate(minLat, maxLat, minLon, maxLon); + } + + @Override + protected boolean queryMatches(byte[] t) { + long a = NumericUtils.sortableBytesToLong(t, 4 * LatLonShape.BYTES); + long b = NumericUtils.sortableBytesToLong(t, 5 * LatLonShape.BYTES); + long c = NumericUtils.sortableBytesToLong(t, 6 * LatLonShape.BYTES); + + int aX = (int)((a >>> 32) & 0x00000000FFFFFFFFL); + int bX = (int)((b >>> 32) & 0x00000000FFFFFFFFL); + int cX = (int)((c >>> 32) & 0x00000000FFFFFFFFL); + int aY = (int)(a & 0x00000000FFFFFFFFL); + int bY = (int)(b & 0x00000000FFFFFFFFL); + int cY = (int)(c & 0x00000000FFFFFFFFL); + + double alat = GeoEncodingUtils.decodeLatitude(aY); + double alon = GeoEncodingUtils.decodeLongitude(aX); + double blat = GeoEncodingUtils.decodeLatitude(bY); + double blon = GeoEncodingUtils.decodeLongitude(bX); + double clat = GeoEncodingUtils.decodeLatitude(cY); + double clon = GeoEncodingUtils.decodeLongitude(cX); + + if (queryRelation == LatLonShape.QueryRelation.WITHIN) { + return line2D.relateTriangle(alon, alat, blon, blat, clon, clat) == Relation.CELL_INSIDE_QUERY; + } + // INTERSECTS + return line2D.relateTriangle(alon, alat, blon, blat, clon, clat) != Relation.CELL_OUTSIDE_QUERY; + } + + @Override + public String toString(String field) { + final StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()); + sb.append(':'); + if (this.field.equals(field) == false) { + sb.append(" field="); + sb.append(this.field); + sb.append(':'); + } + sb.append("Line(" + lines[0].toGeoJSON() + ")"); + return sb.toString(); + } + + @Override + protected boolean equalsTo(Object o) { + return super.equalsTo(o) && Arrays.equals(lines, ((LatLonShapeLineQuery)o).lines); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 31 * hash + Arrays.hashCode(lines); + return hash; + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePolygonQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePolygonQuery.java index a587112b85b..2b342a87da2 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePolygonQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapePolygonQuery.java @@ -29,7 +29,7 @@ import org.apache.lucene.util.NumericUtils; * Finds all previously indexed shapes that intersect the specified arbitrary. * *

The field must be indexed using - * {@link org.apache.lucene.document.LatLonShape#createIndexableFields(String, Polygon)} added per document. + * {@link org.apache.lucene.document.LatLonShape#createIndexableFields} added per document. * * @lucene.experimental **/ diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java index be6b7582579..454b2b87a47 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShapeQuery.java @@ -271,8 +271,8 @@ abstract class LatLonShapeQuery extends Query { return Relation.CELL_CROSSES_QUERY; } - /** utility class for implementing constant score logic specifig to INTERSECT, WITHIN, and DISJOINT */ - protected static abstract class RelationScorerSupplier extends ScorerSupplier { + /** utility class for implementing constant score logic specific to INTERSECT, WITHIN, and DISJOINT */ + private static abstract class RelationScorerSupplier extends ScorerSupplier { PointValues values; IntersectVisitor visitor; long cost = -1; diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java b/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java index c7e626d9519..489e5cf0a3d 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java @@ -98,6 +98,16 @@ public class Line { return lons[vertex]; } + /** Returns a copy of the internal latitude array */ + public double[] getLats() { + return lats.clone(); + } + + /** Returns a copy of the internal longitude array */ + public double[] getLons() { + return lons.clone(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/Line2D.java b/lucene/sandbox/src/java/org/apache/lucene/geo/Line2D.java new file mode 100644 index 00000000000..0f9441fcf3a --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/geo/Line2D.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.geo; + +/** + * 2D line implementation represented as a balanced interval tree of edges. + *

+ * Line {@code Line2D} Construction takes {@code O(n log n)} time for sorting and tree construction. + * {@link #relate relate()} are {@code O(n)}, but for most practical lines are much faster than brute force. + * @lucene.internal + */ +public final class Line2D extends EdgeTree { + + private Line2D(Line line) { + super(line.minLat, line.maxLat, line.minLon, line.maxLon, line.getLats(), line.getLons()); + } + + /** create a Line2D edge tree from provided array of Linestrings */ + public static Line2D create(Line... lines) { + Line2D components[] = new Line2D[lines.length]; + for (int i = 0; i < components.length; ++i) { + components[i] = new Line2D(lines[i]); + } + return (Line2D)createTree(components, 0, components.length - 1, false); + } +} \ No newline at end of file diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java index 35f980d4777..106446e126f 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java @@ -25,6 +25,7 @@ import com.carrotsearch.randomizedtesting.generators.RandomPicks; import org.apache.lucene.document.LatLonShape.QueryRelation; import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Rectangle; @@ -65,6 +66,7 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase { /** name of the LatLonShape indexed field */ protected static final String FIELD_NAME = "shape"; + private static final QueryRelation[] POINT_LINE_RELATIONS = {QueryRelation.INTERSECTS, QueryRelation.DISJOINT}; protected abstract ShapeType getShapeType(); @@ -114,6 +116,26 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase { return new Line(lats, lons); } + /** use {@link GeoTestUtil#nextPolygon()} to create a random line; TODO: move to GeoTestUtil */ + public Line nextLine() { + Polygon poly = GeoTestUtil.nextPolygon(); + double[] lats = new double[poly.numPoints() - 1]; + double[] lons = new double[lats.length]; + System.arraycopy(poly.getPolyLats(), 0, lats, 0, lats.length); + System.arraycopy(poly.getPolyLons(), 0, lons, 0, lons.length); + + return new Line(lats, lons); + } + + /** + * return a semi-random line used for queries + * + * note: shapes parameter may be used to ensure some queries intersect indexed shapes + **/ + protected Line randomQueryLine(Object... shapes) { + return nextLine(); + } + /** creates the array of LatLonShape.Triangle values that are used to index the shape */ protected abstract Field[] createIndexableFields(String field, Object shape); @@ -130,6 +152,11 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase { return LatLonShape.newBoxQuery(field, queryRelation, minLat, maxLat, minLon, maxLon); } + /** factory method to create a new line query */ + protected Query newLineQuery(String field, QueryRelation queryRelation, Line... lines) { + return LatLonShape.newLineQuery(field, queryRelation, lines); + } + /** factory method to create a new polygon query */ protected Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) { return LatLonShape.newPolygonQuery(field, queryRelation, polygons); @@ -209,7 +236,9 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase { // test random bbox queries verifyRandomBBoxQueries(reader, shapes); - // test random polygon queires + // test random line queries + verifyRandomLineQueries(reader, shapes); + // test random polygon queries verifyRandomPolygonQueries(reader, shapes); IOUtils.close(w, reader, dir); @@ -269,7 +298,7 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase { Query query = newRectQuery(FIELD_NAME, queryRelation, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon); if (VERBOSE) { - System.out.println(" query=" + query); + System.out.println(" query=" + query + ", relation=" + queryRelation); } final FixedBitSet hits = new FixedBitSet(maxDoc); @@ -337,6 +366,93 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase { } } + /** test random generated lines */ + protected void verifyRandomLineQueries(IndexReader reader, Object... shapes) throws Exception { + IndexSearcher s = newSearcher(reader); + + final int iters = atLeast(75); + + Bits liveDocs = MultiBits.getLiveDocs(s.getIndexReader()); + int maxDoc = s.getIndexReader().maxDoc(); + + for (int iter = 0; iter < iters; ++iter) { + if (VERBOSE) { + System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s); + } + + // line + Line queryLine = randomQueryLine(shapes); + Line2D queryLine2D = Line2D.create(queryLine); + QueryRelation queryRelation = RandomPicks.randomFrom(random(), POINT_LINE_RELATIONS); + Query query = newLineQuery(FIELD_NAME, queryRelation, queryLine); + + if (VERBOSE) { + System.out.println(" query=" + query + ", relation=" + queryRelation); + } + + final FixedBitSet hits = new FixedBitSet(maxDoc); + s.search(query, new SimpleCollector() { + + private int docBase; + + @Override + public ScoreMode scoreMode() { + return ScoreMode.COMPLETE_NO_SCORES; + } + + @Override + protected void doSetNextReader(LeafReaderContext context) throws IOException { + docBase = context.docBase; + } + + @Override + public void collect(int doc) throws IOException { + hits.set(docBase+doc); + } + }); + + boolean fail = false; + NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id"); + for (int docID = 0; docID < maxDoc; ++docID) { + assertEquals(docID, docIDToID.nextDoc()); + int id = (int) docIDToID.longValue(); + boolean expected; + if (liveDocs != null && liveDocs.get(docID) == false) { + // document is deleted + expected = false; + } else if (shapes[id] == null) { + expected = false; + } else { + expected = getValidator(queryRelation).testLineQuery(queryLine2D, shapes[id]); + } + + if (hits.get(docID) != expected) { + StringBuilder b = new StringBuilder(); + + if (expected) { + b.append("FAIL: id=" + id + " should match but did not\n"); + } else { + b.append("FAIL: id=" + id + " should not match but did\n"); + } + b.append(" relation=" + queryRelation + "\n"); + b.append(" query=" + query + " docID=" + docID + "\n"); + b.append(" shape=" + shapes[id] + "\n"); + b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false)); + b.append(" queryPolygon=" + queryLine.toGeoJSON()); + if (true) { + fail("wrong hit (first of possibly more):\n\n" + b); + } else { + System.out.println(b.toString()); + fail = true; + } + } + } + if (fail) { + fail("some hits were wrong"); + } + } + } + /** test random generated polygons */ protected void verifyRandomPolygonQueries(IndexReader reader, Object... shapes) throws Exception { IndexSearcher s = newSearcher(reader); @@ -358,7 +474,7 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase { Query query = newPolygonQuery(FIELD_NAME, queryRelation, queryPolygon); if (VERBOSE) { - System.out.println(" query=" + query); + System.out.println(" query=" + query + ", relation=" + queryRelation); } final FixedBitSet hits = new FixedBitSet(maxDoc); @@ -500,6 +616,7 @@ public abstract class BaseLatLonShapeTestCase extends LuceneTestCase { protected abstract class Validator { protected QueryRelation queryRelation = QueryRelation.INTERSECTS; public abstract boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape); + public abstract boolean testLineQuery(Line2D line2d, Object shape); public abstract boolean testPolygonQuery(Polygon2D poly2d, Object shape); public void setRelation(QueryRelation relation) { diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java index 9a912322b6f..5c834489795 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java @@ -16,9 +16,12 @@ */ package org.apache.lucene.document; +import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import org.apache.lucene.document.LatLonShape.QueryRelation; +import org.apache.lucene.geo.EdgeTree; +import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.Line; -import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.index.PointValues.Relation; @@ -32,6 +35,32 @@ public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase { return ShapeType.LINE; } + @Override + protected Line randomQueryLine(Object... shapes) { + if (random().nextInt(100) == 42) { + // we want to ensure some cross, so randomly generate lines that share vertices with the indexed point set + int maxBound = (int)Math.floor(shapes.length * 0.1d); + if (maxBound < 2) { + maxBound = shapes.length; + } + double[] lats = new double[RandomNumbers.randomIntBetween(random(), 2, maxBound)]; + double[] lons = new double[lats.length]; + for (int i = 0, j = 0; j < lats.length && i < shapes.length; ++i, ++j) { + Line l = (Line) (shapes[i]); + if (random().nextBoolean() && l != null) { + int v = random().nextInt(l.numPoints() - 1); + lats[j] = l.getLat(v); + lons[j] = l.getLon(v); + } else { + lats[j] = GeoTestUtil.nextLatitude(); + lons[j] = GeoTestUtil.nextLongitude(); + } + } + return new Line(lats, lons); + } + return nextLine(); + } + @Override protected Field[] createIndexableFields(String field, Object line) { return LatLonShape.createIndexableFields(field, (Line)line); @@ -54,9 +83,18 @@ public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase { } // to keep it simple we convert the bbox into a polygon and use poly2d - Polygon2D p = Polygon2D.create(new Polygon[] {new Polygon(new double[] {minLat, minLat, maxLat, maxLat, minLat}, - new double[] {minLon, maxLon, maxLon, minLon, minLon})}); - return testLine(p, l); + Line2D line = Line2D.create(quantizeLine(l)); + Relation r = line.relate(minLat, maxLat, minLon, maxLon); + + if (queryRelation == QueryRelation.DISJOINT) { + return r == Relation.CELL_OUTSIDE_QUERY; + } + return r != Relation.CELL_OUTSIDE_QUERY; + } + + @Override + public boolean testLineQuery(Line2D line2d, Object shape) { + return testLine(line2d, (Line) shape); } @Override @@ -64,7 +102,7 @@ public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase { return testLine(poly2d, (Line) shape); } - private boolean testLine(Polygon2D queryPoly, Line line) { + private boolean testLine(EdgeTree queryPoly, Line line) { double ax, ay, bx, by, temp; Relation r; for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) { diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java index 00bb8d4dbab..3f3c30d89c8 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java @@ -16,7 +16,12 @@ */ package org.apache.lucene.document; +import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import org.apache.lucene.document.LatLonShape.QueryRelation; +import org.apache.lucene.geo.EdgeTree; +import org.apache.lucene.geo.GeoTestUtil; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.index.PointValues.Relation; @@ -35,6 +40,31 @@ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase { return ShapeType.POINT; } + @Override + protected Line randomQueryLine(Object... shapes) { + if (random().nextInt(100) == 42) { + // we want to ensure some cross, so randomly generate lines that share vertices with the indexed point set + int maxBound = (int)Math.floor(shapes.length * 0.1d); + if (maxBound < 2) { + maxBound = shapes.length; + } + double[] lats = new double[RandomNumbers.randomIntBetween(random(), 2, maxBound)]; + double[] lons = new double[lats.length]; + for (int i = 0, j = 0; j < lats.length && i < shapes.length; ++i, ++j) { + Point p = (Point) (shapes[i]); + if (random().nextBoolean() && p != null) { + lats[j] = p.lat; + lons[j] = p.lon; + } else { + lats[j] = GeoTestUtil.nextLatitude(); + lons[j] = GeoTestUtil.nextLongitude(); + } + } + return new Line(lats, lons); + } + return nextLine(); + } + @Override protected Field[] createIndexableFields(String field, Object point) { Point p = (Point)point; @@ -60,13 +90,21 @@ public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase { return isDisjoint == false; } + @Override + public boolean testLineQuery(Line2D line2d, Object shape) { + return testPoint(line2d, (Point) shape); + } + @Override public boolean testPolygonQuery(Polygon2D poly2d, Object shape) { - Point p = (Point) shape; + return testPoint(poly2d, (Point) shape); + } + + private boolean testPoint(EdgeTree tree, Point p) { double lat = decodeLatitude(encodeLatitude(p.lat)); double lon = decodeLongitude(encodeLongitude(p.lon)); // for consistency w/ the query we test the point as a triangle - Relation r = poly2d.relateTriangle(lon, lat, lon, lat, lon, lat); + Relation r = tree.relateTriangle(lon, lat, lon, lat, lon, lat); if (queryRelation == QueryRelation.WITHIN) { return r == Relation.CELL_INSIDE_QUERY; } else if (queryRelation == QueryRelation.DISJOINT) { diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java index 03837a0f029..3a82dd0fab7 100644 --- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java +++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java @@ -19,6 +19,8 @@ package org.apache.lucene.document; import java.util.List; import org.apache.lucene.document.LatLonShape.QueryRelation; +import org.apache.lucene.geo.EdgeTree; +import org.apache.lucene.geo.Line2D; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Polygon2D; import org.apache.lucene.geo.Tessellator; @@ -78,12 +80,21 @@ public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase { return r != Relation.CELL_OUTSIDE_QUERY; } + @Override + public boolean testLineQuery(Line2D query, Object shape) { + return testPolygon(query, (Polygon) shape); + } + @Override public boolean testPolygonQuery(Polygon2D query, Object shape) { - List tessellation = Tessellator.tessellate((Polygon) shape); + return testPolygon(query, (Polygon) shape); + } + + private boolean testPolygon(EdgeTree tree, Polygon shape) { + List tessellation = Tessellator.tessellate(shape); for (Tessellator.Triangle t : tessellation) { // we quantize the triangle for consistency with the index - Relation r = query.relateTriangle(quantizeLon(t.getLon(0)), quantizeLat(t.getLat(0)), + Relation r = tree.relateTriangle(quantizeLon(t.getLon(0)), quantizeLat(t.getLat(0)), quantizeLon(t.getLon(1)), quantizeLat(t.getLat(1)), quantizeLon(t.getLon(2)), quantizeLat(t.getLat(2))); if (queryRelation == QueryRelation.DISJOINT) {