mirror of https://github.com/apache/lucene.git
LUCENE-8554: Add new LatLonShapeLineQuery that queries indexed LatLonShape fields by arbitrary lines
This commit is contained in:
parent
2b8f577c22
commit
0cbefe8b25
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
* 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<EdgeTree> 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Loosely based on the algorithm described in <a href="http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf">
|
||||
* http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf</a>.
|
||||
* @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.
|
||||
* <p>
|
||||
* See <a href="https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html">
|
||||
|
@ -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<Polygon2D> 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
|
||||
* <p>
|
||||
* See <a href="https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html">
|
||||
* https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html</a> 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
|
||||
* <p>
|
||||
* See <a href="https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html">
|
||||
* https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html</a> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
**/
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* <p>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));
|
||||
|
|
|
@ -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}.
|
||||
* <p>
|
||||
* Note:
|
||||
* <ul>
|
||||
* <li>{@code QueryRelation.WITHIN} queries are not yet supported</li>
|
||||
* <li>Dateline crossing is not yet supported</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* todo:
|
||||
* <ul>
|
||||
* <li>Add distance support for buffered queries</li>
|
||||
* </ul>
|
||||
* <p>The field must be indexed using
|
||||
* {@link org.apache.lucene.document.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;
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ import org.apache.lucene.util.NumericUtils;
|
|||
* Finds all previously indexed shapes that intersect the specified arbitrary.
|
||||
*
|
||||
* <p>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
|
||||
**/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<Tessellator.Triangle> tessellation = Tessellator.tessellate((Polygon) shape);
|
||||
return testPolygon(query, (Polygon) shape);
|
||||
}
|
||||
|
||||
private boolean testPolygon(EdgeTree tree, Polygon shape) {
|
||||
List<Tessellator.Triangle> 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) {
|
||||
|
|
Loading…
Reference in New Issue