mirror of https://github.com/apache/lucene.git
LUCENE-8396: Add Points Based Shape Indexing and Search that decomposes shapes into a triangular mesh and indexes individual triangles as a 6 dimension point
This commit is contained in:
parent
424608946c
commit
b5ef13330f
|
@ -169,6 +169,9 @@ Improvements
|
|||
|
||||
* LUCENE-8367: Make per-dimension drill down optional for each facet dimension (Mike McCandless)
|
||||
|
||||
* LUCENE-8396: Add Points Based Shape Indexing and Search that decomposes shapes
|
||||
into a triangular mesh and indexes individual triangles as a 6 dimension point (Nick Knize)
|
||||
|
||||
Other:
|
||||
|
||||
* LUCENE-8366: Upgrade to ICU 62.1. Emoji handling now uses Unicode 11's
|
||||
|
|
|
@ -174,4 +174,42 @@ public final class GeoUtils {
|
|||
return maxLon - lon < 90 && lon - minLon < 90;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a positive value if points a, b, and c are arranged in counter-clockwise order,
|
||||
* negative value if clockwise, zero if collinear.
|
||||
*/
|
||||
// see the "Orient2D" method described here:
|
||||
// http://www.cs.berkeley.edu/~jrs/meshpapers/robnotes.pdf
|
||||
// https://www.cs.cmu.edu/~quake/robust.html
|
||||
// Note that this one does not yet have the floating point tricks to be exact!
|
||||
public static int orient(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
double v1 = (bx - ax) * (cy - ay);
|
||||
double v2 = (cx - ax) * (by - ay);
|
||||
if (v1 > v2) {
|
||||
return 1;
|
||||
} else if (v1 < v2) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* used to define the orientation of 3 points
|
||||
* -1 = Clockwise
|
||||
* 0 = Colinear
|
||||
* 1 = Counter-clockwise
|
||||
**/
|
||||
public enum WindingOrder {
|
||||
CW(-1), COLINEAR(0), CCW(1);
|
||||
private final int sign;
|
||||
WindingOrder(int sign) { this.sign = sign; }
|
||||
public int sign() {return sign;}
|
||||
public static WindingOrder fromSign(final int sign) {
|
||||
if (sign == CW.sign) return CW;
|
||||
if (sign == COLINEAR.sign) return COLINEAR;
|
||||
if (sign == CCW.sign) return CCW;
|
||||
throw new IllegalArgumentException("Invalid WindingOrder sign: " + sign);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ package org.apache.lucene.geo;
|
|||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.lucene.geo.GeoUtils.WindingOrder;
|
||||
|
||||
/**
|
||||
* Represents a closed polygon on the earth's surface. You can either construct the Polygon directly yourself with {@code double[]}
|
||||
* coordinates, or use {@link Polygon#fromGeoJSON} if you have a polygon already encoded as a
|
||||
|
@ -48,6 +50,8 @@ public final class Polygon {
|
|||
public final double minLon;
|
||||
/** maximum longitude of this polygon's bounding box area */
|
||||
public final double maxLon;
|
||||
/** winding order of the vertices */
|
||||
private final WindingOrder windingOrder;
|
||||
|
||||
/**
|
||||
* Creates a new Polygon from the supplied latitude/longitude array, and optionally any holes.
|
||||
|
@ -92,21 +96,32 @@ public final class Polygon {
|
|||
this.holes = holes.clone();
|
||||
|
||||
// compute bounding box
|
||||
double minLat = Double.POSITIVE_INFINITY;
|
||||
double maxLat = Double.NEGATIVE_INFINITY;
|
||||
double minLon = Double.POSITIVE_INFINITY;
|
||||
double maxLon = Double.NEGATIVE_INFINITY;
|
||||
double minLat = polyLats[0];
|
||||
double maxLat = polyLats[0];
|
||||
double minLon = polyLons[0];
|
||||
double maxLon = polyLons[0];
|
||||
|
||||
for (int i = 0;i < polyLats.length; i++) {
|
||||
double windingSum = 0d;
|
||||
final int numPts = polyLats.length - 1;
|
||||
for (int i = 1, j = 0; i < numPts; j = i++) {
|
||||
minLat = Math.min(polyLats[i], minLat);
|
||||
maxLat = Math.max(polyLats[i], maxLat);
|
||||
minLon = Math.min(polyLons[i], minLon);
|
||||
maxLon = Math.max(polyLons[i], maxLon);
|
||||
// compute signed area
|
||||
windingSum += (polyLons[j] - polyLons[numPts])*(polyLats[i] - polyLats[numPts])
|
||||
- (polyLats[j] - polyLats[numPts])*(polyLons[i] - polyLons[numPts]);
|
||||
}
|
||||
this.minLat = minLat;
|
||||
this.maxLat = maxLat;
|
||||
this.minLon = minLon;
|
||||
this.maxLon = maxLon;
|
||||
this.windingOrder = (windingSum < 0) ? GeoUtils.WindingOrder.CW : GeoUtils.WindingOrder.CCW;
|
||||
}
|
||||
|
||||
/** returns the number of vertex points */
|
||||
public int numPoints() {
|
||||
return polyLats.length;
|
||||
}
|
||||
|
||||
/** Returns a copy of the internal latitude array */
|
||||
|
@ -114,16 +129,40 @@ public final class Polygon {
|
|||
return polyLats.clone();
|
||||
}
|
||||
|
||||
/** Returns latitude value at given index */
|
||||
public double getPolyLat(int vertex) {
|
||||
return polyLats[vertex];
|
||||
}
|
||||
|
||||
/** Returns a copy of the internal longitude array */
|
||||
public double[] getPolyLons() {
|
||||
return polyLons.clone();
|
||||
}
|
||||
|
||||
/** Returns longitude value at given index */
|
||||
public double getPolyLon(int vertex) {
|
||||
return polyLons[vertex];
|
||||
}
|
||||
|
||||
/** Returns a copy of the internal holes array */
|
||||
public Polygon[] getHoles() {
|
||||
return holes.clone();
|
||||
}
|
||||
|
||||
Polygon getHole(int i) {
|
||||
return holes[i];
|
||||
}
|
||||
|
||||
/** Returns the winding order (CW, COLINEAR, CCW) for the polygon shell */
|
||||
public WindingOrder getWindingOrder() {
|
||||
return this.windingOrder;
|
||||
}
|
||||
|
||||
/** returns the number of holes for the polygon */
|
||||
public int numHoles() {
|
||||
return holes.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
|
|
|
@ -23,6 +23,8 @@ import org.apache.lucene.geo.Polygon;
|
|||
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>
|
||||
|
@ -454,24 +456,4 @@ public final class Polygon2D {
|
|||
}
|
||||
return newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a positive value if points a, b, and c are arranged in counter-clockwise order,
|
||||
* negative value if clockwise, zero if collinear.
|
||||
*/
|
||||
// see the "Orient2D" method described here:
|
||||
// http://www.cs.berkeley.edu/~jrs/meshpapers/robnotes.pdf
|
||||
// https://www.cs.cmu.edu/~quake/robust.html
|
||||
// Note that this one does not yet have the floating point tricks to be exact!
|
||||
private static int orient(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
double v1 = (bx - ax) * (cy - ay);
|
||||
double v2 = (cx - ax) * (by - ay);
|
||||
if (v1 > v2) {
|
||||
return 1;
|
||||
} else if (v1 < v2) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.lucene.document;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.Tessellator;
|
||||
import org.apache.lucene.geo.Tessellator.Triangle;
|
||||
import org.apache.lucene.index.PointValues;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.util.BytesRef;
|
||||
import org.apache.lucene.util.NumericUtils;
|
||||
|
||||
/**
|
||||
* An indexed shape utility class.
|
||||
* <p>
|
||||
* {@link Polygon}'s are decomposed into a triangular mesh using the {@link Tessellator} utility class
|
||||
* Each {@link Triangle} is encoded and indexed as a multi-value field.
|
||||
* <p>
|
||||
* Finding all shapes that intersect a range (e.g., bounding box) at search time is efficient.
|
||||
* <p>
|
||||
* This class defines static factory methods for common operations:
|
||||
* <ul>
|
||||
* <li>{@link #createIndexableFields(String, Polygon)} for matching polygons that intersect a bounding box.
|
||||
* <li>{@link #newBoxQuery newBoxQuery()} for matching polygons that intersect a bounding box.
|
||||
* </ul>
|
||||
|
||||
* <b>WARNING</b>: Like {@link LatLonPoint}, vertex values are indexed with some loss of precision from the
|
||||
* original {@code double} values (4.190951585769653E-8 for the latitude component
|
||||
* and 8.381903171539307E-8 for longitude).
|
||||
* @see PointValues
|
||||
* @see LatLonDocValuesField
|
||||
*
|
||||
* @lucene.experimental
|
||||
*/
|
||||
public class LatLonShape {
|
||||
public static final int BYTES = LatLonPoint.BYTES;
|
||||
|
||||
protected static final FieldType TYPE = new FieldType();
|
||||
static {
|
||||
TYPE.setDimensions(6, BYTES);
|
||||
TYPE.freeze();
|
||||
}
|
||||
|
||||
// no instance:
|
||||
private LatLonShape() {
|
||||
}
|
||||
|
||||
/** the lionshare of the indexing is done by the tessellator */
|
||||
public static Field[] createIndexableFields(String fieldName, Polygon polygon) {
|
||||
List<Triangle> tessellation = Tessellator.tessellate(polygon);
|
||||
List<LatLonTriangle> fields = new ArrayList<>();
|
||||
for (int i = 0; i < tessellation.size(); ++i) {
|
||||
fields.add(new LatLonTriangle(fieldName, tessellation.get(i)));
|
||||
}
|
||||
return fields.toArray(new Field[fields.size()]);
|
||||
}
|
||||
|
||||
/** create a query to find all polygons that intersect a defined bounding box
|
||||
* note: does not currently support dateline crossing boxes
|
||||
* todo split dateline crossing boxes into two queries like {@link LatLonPoint#newBoxQuery}
|
||||
**/
|
||||
public static Query newBoxQuery(String field, double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) {
|
||||
return new LatLonShapeBoundingBoxQuery(field, minLatitude, maxLatitude, minLongitude, maxLongitude);
|
||||
}
|
||||
|
||||
/** polygons are decomposed into tessellated triangles using {@link org.apache.lucene.geo.Tessellator}
|
||||
* these triangles are encoded and inserted as separate indexed POINT fields
|
||||
*/
|
||||
private static class LatLonTriangle extends Field {
|
||||
|
||||
public LatLonTriangle(String name, Triangle t) {
|
||||
super(name, TYPE);
|
||||
setTriangleValue(t.getEncodedX(0), t.getEncodedY(0),
|
||||
t.getEncodedX(1), t.getEncodedY(1),
|
||||
t.getEncodedX(2), t.getEncodedY(2));
|
||||
}
|
||||
|
||||
public void setTriangleValue(int aX, int aY, int bX, int bY, int cX, int cY) {
|
||||
final byte[] bytes;
|
||||
|
||||
if (fieldsData == null) {
|
||||
bytes = new byte[24];
|
||||
fieldsData = new BytesRef(bytes);
|
||||
} else {
|
||||
bytes = ((BytesRef) fieldsData).bytes;
|
||||
}
|
||||
|
||||
NumericUtils.intToSortableBytes(aY, bytes, 0);
|
||||
NumericUtils.intToSortableBytes(aX, bytes, BYTES);
|
||||
NumericUtils.intToSortableBytes(bY, bytes, BYTES * 2);
|
||||
NumericUtils.intToSortableBytes(bX, bytes, BYTES * 3);
|
||||
NumericUtils.intToSortableBytes(cY, bytes, BYTES * 4);
|
||||
NumericUtils.intToSortableBytes(cX, bytes, BYTES * 5);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,415 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.lucene.document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.Tessellator;
|
||||
import org.apache.lucene.index.LeafReader;
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.index.PointValues;
|
||||
import org.apache.lucene.index.PointValues.IntersectVisitor;
|
||||
import org.apache.lucene.index.PointValues.Relation;
|
||||
import org.apache.lucene.search.ConstantScoreScorer;
|
||||
import org.apache.lucene.search.ConstantScoreWeight;
|
||||
import org.apache.lucene.search.DocIdSetIterator;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.ScoreMode;
|
||||
import org.apache.lucene.search.Scorer;
|
||||
import org.apache.lucene.search.ScorerSupplier;
|
||||
import org.apache.lucene.search.Weight;
|
||||
import org.apache.lucene.util.BitSetIterator;
|
||||
import org.apache.lucene.util.DocIdSetBuilder;
|
||||
import org.apache.lucene.util.FixedBitSet;
|
||||
import org.apache.lucene.util.FutureArrays;
|
||||
import org.apache.lucene.util.NumericUtils;
|
||||
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @lucene.experimental
|
||||
**/
|
||||
class LatLonShapeBoundingBoxQuery extends Query {
|
||||
final String field;
|
||||
final byte[] bbox;
|
||||
final int minX;
|
||||
final int maxX;
|
||||
final int minY;
|
||||
final int maxY;
|
||||
|
||||
public LatLonShapeBoundingBoxQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
|
||||
if (minLon > maxLon) {
|
||||
throw new IllegalArgumentException("dateline crossing bounding box queries are not supported for [" + field + "]");
|
||||
}
|
||||
this.field = field;
|
||||
this.bbox = new byte[4 * LatLonPoint.BYTES];
|
||||
this.minX = encodeLongitudeCeil(minLon);
|
||||
this.maxX = encodeLongitude(maxLon);
|
||||
this.minY = encodeLatitudeCeil(minLat);
|
||||
this.maxY = encodeLatitude(maxLat);
|
||||
NumericUtils.intToSortableBytes(this.minY, this.bbox, 0);
|
||||
NumericUtils.intToSortableBytes(this.minX, this.bbox, LatLonPoint.BYTES);
|
||||
NumericUtils.intToSortableBytes(this.maxY, this.bbox, 2 * LatLonPoint.BYTES);
|
||||
NumericUtils.intToSortableBytes(this.maxX, this.bbox, 3 * LatLonPoint.BYTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
|
||||
return new ConstantScoreWeight(this, boost) {
|
||||
|
||||
private boolean queryContains(byte[] t, int point) {
|
||||
final int yIdx = 2 * LatLonPoint.BYTES * point;
|
||||
final int xIdx = yIdx + LatLonPoint.BYTES;
|
||||
|
||||
if (FutureArrays.compareUnsigned(t, yIdx, xIdx, bbox, 0, LatLonPoint.BYTES) < 0 || //minY
|
||||
FutureArrays.compareUnsigned(t, yIdx, xIdx, bbox, 2 * LatLonPoint.BYTES, 3 * LatLonPoint.BYTES) > 0 || //maxY
|
||||
FutureArrays.compareUnsigned(t, xIdx, xIdx + LatLonPoint.BYTES, bbox, LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) < 0 || // minX
|
||||
FutureArrays.compareUnsigned(t, xIdx, xIdx + LatLonPoint.BYTES, bbox, 3 * LatLonPoint.BYTES, bbox.length) > 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean queryIntersects(int ax, int ay, int bx, int by, int cx, int cy) {
|
||||
// top
|
||||
if (Tessellator.linesIntersect(minX, maxY, maxX, maxY, ax, ay, bx, by) ||
|
||||
Tessellator.linesIntersect(minX, maxY, maxX, maxY, bx, by, cx, cy) ||
|
||||
Tessellator.linesIntersect(minX, maxY, maxX, maxY, cx, cy, ax, ay)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// bottom
|
||||
if (Tessellator.linesIntersect(minX, minY, maxX, minY, ax, ay, bx, by) ||
|
||||
Tessellator.linesIntersect(minX, minY, maxX, minY, bx, by, cx, cy) ||
|
||||
Tessellator.linesIntersect(minX, minY, maxX, minY, cx, cy, ax, ay)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// left
|
||||
if (Tessellator.linesIntersect(minX, minY, minX, maxY, ax, ay, bx, by) ||
|
||||
Tessellator.linesIntersect(minX, minY, minX, maxY, bx, by, cx, cy) ||
|
||||
Tessellator.linesIntersect(minX, minY, minX, maxY, cx, cy, ax, ay)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// right
|
||||
if (Tessellator.linesIntersect(maxX, minY, maxX, maxY, ax, ay, bx, by) ||
|
||||
Tessellator.linesIntersect(maxX, minY, maxX, maxY, bx, by, cx, cy) ||
|
||||
Tessellator.linesIntersect(maxX, minY, maxX, maxY, cx, cy, ax, ay)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean queryCrossesTriangle(byte[] t) {
|
||||
// 1. query contains any triangle points
|
||||
if (queryContains(t, 0) || queryContains(t, 1) || queryContains(t, 2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int aY = NumericUtils.sortableBytesToInt(t, 0);
|
||||
int aX = NumericUtils.sortableBytesToInt(t, LatLonPoint.BYTES);
|
||||
int bY = NumericUtils.sortableBytesToInt(t, 2 * LatLonPoint.BYTES);
|
||||
int bX = NumericUtils.sortableBytesToInt(t, 3 * LatLonPoint.BYTES);
|
||||
int cY = NumericUtils.sortableBytesToInt(t, 4 * LatLonPoint.BYTES);
|
||||
int cX = NumericUtils.sortableBytesToInt(t, 5 * LatLonPoint.BYTES);
|
||||
|
||||
int tMinX = StrictMath.min(StrictMath.min(aX, bX), cX);
|
||||
int tMaxX = StrictMath.max(StrictMath.max(aX, bX), cX);
|
||||
int tMinY = StrictMath.min(StrictMath.min(aY, bY), cY);
|
||||
int tMaxY = StrictMath.max(StrictMath.max(aY, bY), cY);
|
||||
|
||||
// 2. check bounding boxes are disjoint
|
||||
if (tMaxX < minX || tMinX > maxX || tMinY > maxY || tMaxY < minY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. check triangle contains any query points
|
||||
if (Tessellator.pointInTriangle(minX, minY, aX, aY, bX, bY, cX, cY)) {
|
||||
return true;
|
||||
} else if (Tessellator.pointInTriangle(maxX, minY, aX, aY, bX, bY, cX, cY)) {
|
||||
return true;
|
||||
} else if (Tessellator.pointInTriangle(maxX, maxY, aX, aY, bX, bY, cX, cY)) {
|
||||
return true;
|
||||
} else if (Tessellator.pointInTriangle(minX, maxY, aX, aY, bX, bY, cX, cY)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// 4. last ditch effort: check crossings
|
||||
if (queryIntersects(aX, aY, bX, bY, cX, cY)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Relation relateRangeToQuery(byte[] minTriangle, byte[] maxTriangle) {
|
||||
// compute bounding box
|
||||
int minXOfs = 0;
|
||||
int minYOfs = 0;
|
||||
int maxXOfs = 0;
|
||||
int maxYOfs = 0;
|
||||
for (int d = 1; d < 3; ++d) {
|
||||
// check minX
|
||||
int aOfs = (minXOfs * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
int bOfs = (d * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
if (FutureArrays.compareUnsigned(minTriangle, bOfs, bOfs + LatLonPoint.BYTES, minTriangle, aOfs, aOfs + LatLonPoint.BYTES) < 0) {
|
||||
minXOfs = d;
|
||||
}
|
||||
// check maxX
|
||||
aOfs = (maxXOfs * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
if (FutureArrays.compareUnsigned(maxTriangle, bOfs, bOfs + LatLonPoint.BYTES, maxTriangle, aOfs, aOfs + LatLonPoint.BYTES) > 0) {
|
||||
maxXOfs = d;
|
||||
}
|
||||
// check minY
|
||||
aOfs = minYOfs * 2 * LatLonPoint.BYTES;
|
||||
bOfs = d * 2 * LatLonPoint.BYTES;
|
||||
if (FutureArrays.compareUnsigned(minTriangle, bOfs, bOfs + LatLonPoint.BYTES, minTriangle, aOfs, aOfs + LatLonPoint.BYTES) < 0) {
|
||||
minYOfs = d;
|
||||
}
|
||||
// check maxY
|
||||
aOfs = maxYOfs * 2 * LatLonPoint.BYTES;
|
||||
if (FutureArrays.compareUnsigned(maxTriangle, bOfs, bOfs + LatLonPoint.BYTES, maxTriangle, aOfs, aOfs + LatLonPoint.BYTES) > 0) {
|
||||
maxYOfs = d;
|
||||
}
|
||||
}
|
||||
minXOfs = (minXOfs * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
maxXOfs = (maxXOfs * 2 * LatLonPoint.BYTES) + LatLonPoint.BYTES;
|
||||
minYOfs *= 2 * LatLonPoint.BYTES;
|
||||
maxYOfs *= 2 * LatLonPoint.BYTES;
|
||||
|
||||
// check bounding box (DISJOINT)
|
||||
if (FutureArrays.compareUnsigned(minTriangle, minXOfs, minXOfs + LatLonPoint.BYTES, bbox, 3 * LatLonPoint.BYTES, 4 * LatLonPoint.BYTES) > 0 ||
|
||||
FutureArrays.compareUnsigned(maxTriangle, maxXOfs, maxXOfs + LatLonPoint.BYTES, bbox, LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) < 0 ||
|
||||
FutureArrays.compareUnsigned(minTriangle, minYOfs, minYOfs + LatLonPoint.BYTES, bbox, 2 * LatLonPoint.BYTES, 3 * LatLonPoint.BYTES) > 0 ||
|
||||
FutureArrays.compareUnsigned(maxTriangle, maxYOfs, maxYOfs + LatLonPoint.BYTES, bbox, 0, LatLonPoint.BYTES) < 0) {
|
||||
return Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
if (FutureArrays.compareUnsigned(minTriangle, minXOfs, minXOfs + LatLonPoint.BYTES, bbox, LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) > 0 &&
|
||||
FutureArrays.compareUnsigned(maxTriangle, maxXOfs, maxXOfs + LatLonPoint.BYTES, bbox, 3 * LatLonPoint.BYTES, 4 * LatLonPoint.BYTES) < 0 &&
|
||||
FutureArrays.compareUnsigned(minTriangle, minYOfs, minYOfs + LatLonPoint.BYTES, bbox, 0, LatLonPoint.BYTES) > 0 &&
|
||||
FutureArrays.compareUnsigned(maxTriangle, maxYOfs, maxYOfs + LatLonPoint.BYTES, bbox, 2 * LatLonPoint.BYTES, 2 * LatLonPoint.BYTES) < 0) {
|
||||
return Relation.CELL_INSIDE_QUERY;
|
||||
}
|
||||
return Relation.CELL_CROSSES_QUERY;
|
||||
}
|
||||
|
||||
private IntersectVisitor getIntersectVisitor(DocIdSetBuilder result) {
|
||||
return new IntersectVisitor() {
|
||||
|
||||
DocIdSetBuilder.BulkAdder adder;
|
||||
|
||||
@Override
|
||||
public void grow(int count) {
|
||||
adder = result.grow(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int docID) throws IOException {
|
||||
adder.add(docID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int docID, byte[] t) throws IOException {
|
||||
if (queryCrossesTriangle(t)) {
|
||||
adder.add(docID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Relation compare(byte[] minTriangle, byte[] maxTriangle) {
|
||||
return relateRangeToQuery(minTriangle, maxTriangle);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a visitor that clears documents that do NOT match the bounding box query.
|
||||
*/
|
||||
private IntersectVisitor getInverseIntersectVisitor(FixedBitSet result, int[] cost) {
|
||||
return new IntersectVisitor() {
|
||||
|
||||
@Override
|
||||
public void visit(int docID) {
|
||||
result.clear(docID);
|
||||
cost[0]--;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int docID, byte[] packedTriangle) {
|
||||
if (queryCrossesTriangle(packedTriangle)) {
|
||||
result.clear(docID);
|
||||
cost[0]--;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
|
||||
Relation r = relateRangeToQuery(minPackedValue, maxPackedValue);
|
||||
if (r == Relation.CELL_OUTSIDE_QUERY) {
|
||||
return Relation.CELL_INSIDE_QUERY;
|
||||
} else if (r == Relation.CELL_INSIDE_QUERY || r == Relation.CELL_CROSSES_QUERY) {
|
||||
return Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
|
||||
LeafReader reader = context.reader();
|
||||
PointValues values = reader.getPointValues(field);
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean allDocsMatch = true;
|
||||
if (values.getDocCount() != reader.maxDoc() ||
|
||||
relateRangeToQuery(values.getMinPackedValue(), values.getMaxPackedValue()) != Relation.CELL_INSIDE_QUERY) {
|
||||
allDocsMatch = false;
|
||||
}
|
||||
|
||||
final Weight weight = this;
|
||||
if (allDocsMatch) {
|
||||
return new ScorerSupplier() {
|
||||
@Override
|
||||
public Scorer get(long leadCost) throws IOException {
|
||||
return new ConstantScoreScorer(weight, score(),
|
||||
DocIdSetIterator.all(reader.maxDoc()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long cost() {
|
||||
return reader.maxDoc();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return new ScorerSupplier() {
|
||||
final DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field);
|
||||
final IntersectVisitor visitor = getIntersectVisitor(result);
|
||||
long cost = -1;
|
||||
|
||||
@Override
|
||||
public Scorer get(long leadCost) throws IOException {
|
||||
if (values.getDocCount() == reader.maxDoc()
|
||||
&& values.getDocCount() == values.size()
|
||||
&& cost() > reader.maxDoc() / 2) {
|
||||
// If all docs have exactly one value and the cost is greater
|
||||
// than half the leaf size then maybe we can make things faster
|
||||
// by computing the set of documents that do NOT match the query
|
||||
final FixedBitSet result = new FixedBitSet(reader.maxDoc());
|
||||
result.set(0, reader.maxDoc());
|
||||
int[] cost = new int[]{reader.maxDoc()};
|
||||
values.intersect(getInverseIntersectVisitor(result, cost));
|
||||
final DocIdSetIterator iterator = new BitSetIterator(result, cost[0]);
|
||||
return new ConstantScoreScorer(weight, score(), iterator);
|
||||
}
|
||||
|
||||
values.intersect(visitor);
|
||||
DocIdSetIterator iterator = result.build().iterator();
|
||||
return new ConstantScoreScorer(weight, score(), iterator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long cost() {
|
||||
if (cost == -1) {
|
||||
// Computing the cost may be expensive, so only do it if necessary
|
||||
cost = values.estimatePointCount(visitor);
|
||||
assert cost >= 0;
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scorer scorer(LeafReaderContext context) throws IOException {
|
||||
ScorerSupplier scorerSupplier = scorerSupplier(context);
|
||||
if (scorerSupplier == null) {
|
||||
return null;
|
||||
}
|
||||
return scorerSupplier.get(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCacheable(LeafReaderContext ctx) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return sameClassAs(o) && equalsTo(getClass().cast(o));
|
||||
}
|
||||
|
||||
private boolean equalsTo(LatLonShapeBoundingBoxQuery o) {
|
||||
return Objects.equals(field, o.field) &&
|
||||
Arrays.equals(bbox, o.bbox);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = classHash();
|
||||
hash = 31 * hash + field.hashCode();
|
||||
hash = 31 * hash + Arrays.hashCode(bbox);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String field) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(getClass().getSimpleName());
|
||||
sb.append(':');
|
||||
if (this.field.equals(field) == false) {
|
||||
sb.append(" field=");
|
||||
sb.append(this.field);
|
||||
sb.append(':');
|
||||
}
|
||||
sb.append("Rectangle(lat=");
|
||||
sb.append(decodeLatitude(minY));
|
||||
sb.append(" TO ");
|
||||
sb.append(decodeLatitude(maxY));
|
||||
sb.append(" lon=");
|
||||
sb.append(decodeLongitude(minX));
|
||||
sb.append(" TO ");
|
||||
sb.append(decodeLongitude(maxX));
|
||||
sb.append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,910 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.lucene.geo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.lucene.geo.GeoUtils.WindingOrder;
|
||||
import org.apache.lucene.util.BitUtil;
|
||||
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoUtils.orient;
|
||||
|
||||
/**
|
||||
* Computes a triangular mesh tessellation for a given polygon.
|
||||
* <p>
|
||||
* This is inspired by mapbox's earcut algorithm (https://github.com/mapbox/earcut)
|
||||
* which is a modification to FIST (https://www.cosy.sbg.ac.at/~held/projects/triang/triang.html)
|
||||
* written by Martin Held, and ear clipping (https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf)
|
||||
* written by David Eberly.
|
||||
* <p>
|
||||
* Notes:
|
||||
* <ul>
|
||||
* <li>Requires valid polygons:
|
||||
* <ul>
|
||||
* <li>No self intersections
|
||||
* <li>Holes may only touch at one vertex
|
||||
* <li>Polygon must have an area (e.g., no "line" boxes)
|
||||
* <li>sensitive to overflow (e.g, subatomic values such as E-200 can cause unexpected behavior)
|
||||
* </ul>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The code is a modified version of the javascript implementation provided by MapBox
|
||||
* under the following license:
|
||||
* <p>
|
||||
* ISC License
|
||||
* <p>
|
||||
* Copyright (c) 2016, Mapbox
|
||||
* <p>
|
||||
* Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
* with or without fee is hereby granted, provided that the above copyright notice
|
||||
* and this permission notice appear in all copies.
|
||||
* <p>
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH'
|
||||
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
* THIS SOFTWARE.
|
||||
*
|
||||
* @lucene.experimental
|
||||
*/
|
||||
final public class Tessellator {
|
||||
// this is a dumb heuristic to control whether we cut over to sorted morton values
|
||||
private static final int VERTEX_THRESHOLD = 80;
|
||||
|
||||
/** state of the tessellated split - avoids recursion */
|
||||
private enum State {
|
||||
INIT, CURE, SPLIT
|
||||
}
|
||||
|
||||
// No Instance:
|
||||
private Tessellator() {}
|
||||
|
||||
/** Produces an array of vertices representing the triangulated result set of the Points array */
|
||||
public static final List<Triangle> tessellate(final Polygon polygon) {
|
||||
// Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but this will correct);
|
||||
// then filter instances of intersections.
|
||||
Node outerNode = createDoublyLinkedList(polygon, WindingOrder.CCW);
|
||||
// If an outer node hasn't been detected, the shape is malformed. (must comply with OGC SFA specification)
|
||||
if(outerNode == null) {
|
||||
throw new IllegalArgumentException("Malformed shape detected in Tessellator!");
|
||||
}
|
||||
|
||||
// Determine if the specified list of points contains holes
|
||||
if (polygon.numHoles() > 0) {
|
||||
// Eliminate the hole triangulation.
|
||||
outerNode = eliminateHoles(polygon, outerNode);
|
||||
}
|
||||
|
||||
// If the shape crosses VERTEX_THRESHOLD, use z-order curve hashing:
|
||||
final boolean mortonOptimized;
|
||||
{
|
||||
int threshold = VERTEX_THRESHOLD - polygon.numPoints();
|
||||
for (int i = 0; threshold >= 0 && i < polygon.numHoles(); ++i) {
|
||||
threshold -= polygon.getHole(i).numPoints();
|
||||
}
|
||||
|
||||
// Link polygon nodes in Z-Order
|
||||
mortonOptimized = threshold < 0;
|
||||
if (mortonOptimized == true) {
|
||||
sortByMorton(outerNode);
|
||||
}
|
||||
}
|
||||
// Calculate the tessellation using the doubly LinkedList.
|
||||
List<Triangle> result = earcutLinkedList(outerNode, new ArrayList<>(), State.INIT, mortonOptimized);
|
||||
if (result.size() == 0) {
|
||||
throw new IllegalArgumentException("Unable to Tessellate shape [" + polygon + "]. Possible malformed shape detected.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Creates a circular doubly linked list using polygon points. The order is governed by the specified winding order */
|
||||
private static final Node createDoublyLinkedList(final Polygon polygon, final WindingOrder windingOrder) {
|
||||
Node lastNode = null;
|
||||
// Link points into the circular doubly-linked list in the specified winding order
|
||||
if (windingOrder == polygon.getWindingOrder()) {
|
||||
for (int i = 0; i < polygon.numPoints(); ++i) {
|
||||
if (lastNode == null || filter(polygon, i, lastNode) == false) {
|
||||
lastNode = insertNode(polygon, i, lastNode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = polygon.numPoints() - 1; i >= 0; --i) {
|
||||
if (lastNode == null || filter(polygon, i, lastNode) == false) {
|
||||
lastNode = insertNode(polygon, i, lastNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if first and last node are the same then remove the end node and set lastNode to the start
|
||||
if (lastNode != null && isVertexEquals(lastNode, lastNode.next)) {
|
||||
removeNode(lastNode);
|
||||
lastNode = lastNode.next;
|
||||
}
|
||||
|
||||
// Return the last node in the Doubly-Linked List
|
||||
return lastNode;
|
||||
}
|
||||
|
||||
/** Links every hole into the outer loop, producing a single-ring polygon without holes. **/
|
||||
private static final Node eliminateHoles(final Polygon polygon, Node outerNode) {
|
||||
// Define a list to hole a reference to each filtered hole list.
|
||||
final List<Node> holeList = new ArrayList<>();
|
||||
// Iterate through each array of hole vertices.
|
||||
Polygon[] holes = polygon.getHoles();
|
||||
for(int i = 0; i < polygon.numHoles(); ++i) {
|
||||
// create the doubly-linked hole list
|
||||
Node list = createDoublyLinkedList(holes[i], WindingOrder.CW);
|
||||
if (list == list.next) {
|
||||
list.isSteiner = true;
|
||||
}
|
||||
// Determine if the resulting hole polygon was successful.
|
||||
if(list != null) {
|
||||
// Add the leftmost vertex of the hole.
|
||||
holeList.add(fetchLeftmost(list));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the hole vertices by x coordinate
|
||||
holeList.sort((Node pNodeA, Node pNodeB) ->
|
||||
pNodeA.getX() < pNodeB.getX() ? -1 : pNodeA.getX() == pNodeB.getX() ? 0 : 1);
|
||||
|
||||
// Process holes from left to right.
|
||||
for(int i = 0; i < holeList.size(); ++i) {
|
||||
// Eliminate hole triangles from the result set
|
||||
final Node holeNode = holeList.get(i);
|
||||
eliminateHole(holeNode, outerNode);
|
||||
// Filter the new polygon.
|
||||
outerNode = filterPoints(outerNode, outerNode.next);
|
||||
}
|
||||
// Return a pointer to the list.
|
||||
return outerNode;
|
||||
}
|
||||
|
||||
/** Finds a bridge between vertices that connects a hole with an outer ring, and links it */
|
||||
private static final void eliminateHole(final Node holeNode, Node outerNode) {
|
||||
// Attempt to find a logical bridge between the HoleNode and OuterNode.
|
||||
outerNode = fetchHoleBridge(holeNode, outerNode);
|
||||
// Determine whether a hole bridge could be fetched.
|
||||
if(outerNode != null) {
|
||||
// Split the resulting polygon.
|
||||
Node node = splitPolygon(outerNode, holeNode);
|
||||
// Filter the split nodes.
|
||||
filterPoints(node, node.next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* David Eberly's algorithm for finding a bridge between a hole and outer polygon
|
||||
*
|
||||
* see: http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
|
||||
**/
|
||||
private static final Node fetchHoleBridge(final Node holeNode, final Node outerNode) {
|
||||
Node p = outerNode;
|
||||
double qx = Double.NEGATIVE_INFINITY;
|
||||
final double hx = holeNode.getX();
|
||||
final double hy = holeNode.getY();
|
||||
Node connection = null;
|
||||
// 1. find a segment intersected by a ray from the hole's leftmost point to the left;
|
||||
// segment's endpoint with lesser x will be potential connection point
|
||||
{
|
||||
do {
|
||||
if (hy <= p.getY() && hy >= p.next.getY()) {
|
||||
final double x = p.getX() + (hy - p.getY()) * (p.next.getX() - p.getX()) / (p.next.getY() - p.getY());
|
||||
if (x <= hx && x > qx) {
|
||||
qx = x;
|
||||
connection = (p.getX() < p.next.getX()) ? p : p.next;
|
||||
}
|
||||
}
|
||||
p = p.next;
|
||||
} while (p != outerNode);
|
||||
}
|
||||
|
||||
if (connection == null) {
|
||||
return null;
|
||||
} else if (hx == connection.getX()) {
|
||||
return connection.previous;
|
||||
}
|
||||
|
||||
// 2. look for points inside the triangle of hole point, segment intersection, and endpoint
|
||||
// its a valid connection iff there are no points found;
|
||||
// otherwise choose the point of the minimum angle with the ray as the connection point
|
||||
Node stop = connection;
|
||||
final double mx = connection.getX();
|
||||
final double my = connection.getY();
|
||||
double tanMin = Double.POSITIVE_INFINITY;
|
||||
double tan;
|
||||
p = connection.next;
|
||||
{
|
||||
while (p != stop) {
|
||||
if (hx > p.getX() && p.getX() >= mx && pointInEar(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, mx, my)) {
|
||||
tan = Math.abs(hy - my) / (hx - mx); // tangential
|
||||
if ((tan < tanMin || (tan == tanMin && p.getX() > connection.getX())) && isLocallyInside(p, holeNode)) {
|
||||
connection = p;
|
||||
tanMin = tan;
|
||||
}
|
||||
}
|
||||
p = p.next;
|
||||
}
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/** Finds the left-most hole of a polygon ring. **/
|
||||
private static final Node fetchLeftmost(final Node start) {
|
||||
Node node = start;
|
||||
Node leftMost = start;
|
||||
do {
|
||||
// Determine if the current node possesses a lesser X position.
|
||||
if (node.getX() < leftMost.getX()) {
|
||||
// Maintain a reference to this Node.
|
||||
leftMost = node;
|
||||
}
|
||||
// Progress the search to the next node in the doubly-linked list.
|
||||
node = node.next;
|
||||
} while (node != start);
|
||||
|
||||
// Return the node with the smallest X value.
|
||||
return leftMost;
|
||||
}
|
||||
|
||||
/** Main ear slicing loop which triangulates the vertices of a polygon, provided as a doubly-linked list. **/
|
||||
private static final List<Triangle> earcutLinkedList(Node currEar, final List<Triangle> tessellation,
|
||||
State state, final boolean mortonOptimized) {
|
||||
earcut : do {
|
||||
if (currEar == null || currEar.previous == currEar.next) {
|
||||
return tessellation;
|
||||
}
|
||||
|
||||
Node stop = currEar;
|
||||
Node prevNode;
|
||||
Node nextNode;
|
||||
|
||||
// Iteratively slice ears
|
||||
do {
|
||||
prevNode = currEar.previous;
|
||||
nextNode = currEar.next;
|
||||
// Determine whether the current triangle must be cut off.
|
||||
final boolean isReflex = area(prevNode.getX(), prevNode.getY(), currEar.getX(), currEar.getY(), nextNode.getX(), nextNode.getY()) >= 0;
|
||||
if (isReflex == false && isEar(currEar, mortonOptimized) == true) {
|
||||
// Return the triangulated data
|
||||
tessellation.add(new Triangle(prevNode, currEar, nextNode));
|
||||
// Remove the ear node.
|
||||
removeNode(currEar);
|
||||
|
||||
// Skipping to the next node leaves fewer slither triangles.
|
||||
currEar = nextNode.next;
|
||||
stop = nextNode.next;
|
||||
continue;
|
||||
}
|
||||
currEar = nextNode;
|
||||
|
||||
// If the whole polygon has been iterated over and no more ears can be found.
|
||||
if (currEar == stop) {
|
||||
switch (state) {
|
||||
case INIT:
|
||||
// try filtering points and slicing again
|
||||
currEar = filterPoints(currEar, null);
|
||||
state = State.CURE;
|
||||
continue earcut;
|
||||
case CURE:
|
||||
// if this didn't work, try curing all small self-intersections locally
|
||||
currEar = cureLocalIntersections(currEar, tessellation);
|
||||
state = State.SPLIT;
|
||||
continue earcut;
|
||||
case SPLIT:
|
||||
// as a last resort, try splitting the remaining polygon into two
|
||||
splitEarcut(currEar, tessellation, mortonOptimized);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} while (currEar.previous != currEar.next);
|
||||
break;
|
||||
} while (true);
|
||||
// Return the calculated tessellation
|
||||
return tessellation;
|
||||
}
|
||||
|
||||
/** Determines whether a polygon node forms a valid ear with adjacent nodes. **/
|
||||
private static final boolean isEar(final Node ear, final boolean mortonOptimized) {
|
||||
if (mortonOptimized == true) {
|
||||
return mortonIsEar(ear);
|
||||
}
|
||||
|
||||
// make sure there aren't other points inside the potential ear
|
||||
Node node = ear.next.next;
|
||||
while (node != ear.previous) {
|
||||
if (pointInEar(node.getX(), node.getY(), ear.previous.getX(), ear.previous.getY(), ear.getX(), ear.getY(), ear.next.getX(), ear.next.getY())
|
||||
&& area(node.previous.getX(), node.previous.getY(), node.getX(), node.getY(),
|
||||
node.next.getX(), node.next.getY()) >= 0) {
|
||||
return false;
|
||||
}
|
||||
node = node.next;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Uses morton code for speed to determine whether or a polygon node forms a valid ear w/ adjacent nodes */
|
||||
private static final boolean mortonIsEar(final Node ear) {
|
||||
double ax = ear.previous.x;
|
||||
double ay = ear.previous.y;
|
||||
double bx = ear.x;
|
||||
double by = ear.y;
|
||||
double cx = ear.next.x;
|
||||
double cy = ear.next.y;
|
||||
|
||||
// now make sure we don't have other points inside the potential ear;
|
||||
Node node;
|
||||
int idx;
|
||||
|
||||
// triangle bbox (flip the bits so negative encoded values are < positive encoded values)
|
||||
int minTX = StrictMath.min(StrictMath.min(ear.previous.x, ear.x), ear.next.x) ^ 0x80000000;
|
||||
int minTY = StrictMath.min(StrictMath.min(ear.previous.y, ear.y), ear.next.y) ^ 0x80000000;
|
||||
int maxTX = StrictMath.max(StrictMath.max(ear.previous.x, ear.x), ear.next.x) ^ 0x80000000;
|
||||
int maxTY = StrictMath.max(StrictMath.max(ear.previous.y, ear.y), ear.next.y) ^ 0x80000000;
|
||||
|
||||
// z-order range for the current triangle bbox;
|
||||
long minZ = BitUtil.interleave(minTX, minTY);
|
||||
long maxZ = BitUtil.interleave(maxTX, maxTY);
|
||||
|
||||
// first look for points inside the triangle in increasing z-order
|
||||
node = ear.nextZ;
|
||||
while (node != null && Long.compareUnsigned(node.morton, maxZ) <= 0) {
|
||||
if (Long.compareUnsigned(node.morton, maxZ) <= 0) {
|
||||
idx = node.idx;
|
||||
if (idx != ear.previous.idx && idx != ear.next.idx
|
||||
&& pointInEar(node.x, node.y, ax, ay, bx, by, cx, cy)
|
||||
&& area(node.previous.x, node.previous.y, node.x, node.y, node.next.x, node.next.y) >= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
node = node.nextZ;
|
||||
}
|
||||
// then look for points in decreasing z-order
|
||||
node = ear.previousZ;
|
||||
while (node != null &&
|
||||
Long.compareUnsigned(node.morton, minZ) >= 0) {
|
||||
if (Long.compareUnsigned(node.morton, maxZ) <= 0) {
|
||||
idx = node.idx;
|
||||
if (idx != ear.previous.idx && idx != ear.next.idx
|
||||
&& pointInEar(node.x, node.y, ax, ay, bx, by, cx, cy)
|
||||
&& area(node.previous.x, node.previous.y, node.x, node.y, node.next.x, node.next.y) >= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
node = node.previousZ;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Iterate through all polygon nodes and remove small local self-intersections **/
|
||||
private static final Node cureLocalIntersections(Node startNode, final List<Triangle> tessellation) {
|
||||
Node node = startNode;
|
||||
Node nextNode;
|
||||
do {
|
||||
nextNode = node.next;
|
||||
Node a = node.previous;
|
||||
Node b = nextNode.next;
|
||||
|
||||
// a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2])
|
||||
if (isVertexEquals(a, b) == false
|
||||
&& linesIntersect(a.getX(), a.getY(), node.getX(), node.getY(), nextNode.getX(), nextNode.getY(), b.getX(), b.getY())
|
||||
&& isLocallyInside(a, b) && isLocallyInside(b, a)) {
|
||||
// Return the triangulated vertices to the tessellation
|
||||
tessellation.add(new Triangle(a, node, b));
|
||||
|
||||
// remove two nodes involved
|
||||
removeNode(node);
|
||||
removeNode(node.next);
|
||||
node = startNode = b;
|
||||
}
|
||||
node = node.next;
|
||||
} while (node != startNode);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Attempt to split a polygon and independently triangulate each side **/
|
||||
private static final void splitEarcut(final Node start, final List<Triangle> tessellation, final boolean mortonIndexed) {
|
||||
// Search for a valid diagonal that divides the polygon into two.
|
||||
Node searchNode = start;
|
||||
Node nextNode;
|
||||
do {
|
||||
nextNode = searchNode.next;
|
||||
Node diagonal = nextNode.next;
|
||||
while (diagonal != searchNode.previous) {
|
||||
if(isValidDiagonal(searchNode, diagonal)) {
|
||||
// Split the polygon into two at the point of the diagonal
|
||||
Node splitNode = splitPolygon(searchNode, diagonal);
|
||||
// Filter the resulting polygon.
|
||||
searchNode = filterPoints(searchNode, searchNode.next);
|
||||
splitNode = filterPoints(splitNode, splitNode.next);
|
||||
// Attempt to earcut both of the resulting polygons
|
||||
earcutLinkedList(searchNode, tessellation, State.INIT, mortonIndexed);
|
||||
earcutLinkedList(splitNode, tessellation, State.INIT, mortonIndexed);
|
||||
// Finish the iterative search
|
||||
return;
|
||||
}
|
||||
diagonal = diagonal.next;
|
||||
}
|
||||
searchNode = searchNode.next;
|
||||
} while (searchNode != start);
|
||||
}
|
||||
|
||||
/** Links two polygon vertices using a bridge. **/
|
||||
private static final Node splitPolygon(final Node a, final Node b) {
|
||||
final Node a2 = new Node(a);
|
||||
final Node b2 = new Node(b);
|
||||
final Node an = a.next;
|
||||
final Node bp = b.previous;
|
||||
|
||||
a.next = b;
|
||||
a.nextZ = b;
|
||||
b.previous = a;
|
||||
b.previousZ = a;
|
||||
an.previous = a2;
|
||||
an.previousZ = a2;
|
||||
b2.next = a2;
|
||||
b2.nextZ = a2;
|
||||
a2.previous = b2;
|
||||
a2.previousZ = b2;
|
||||
bp.next = b2;
|
||||
bp.nextZ = b2;
|
||||
|
||||
return b2;
|
||||
}
|
||||
|
||||
/** Determines whether a diagonal between two polygon nodes lies within a polygon interior. (This determines the validity of the ray.) **/
|
||||
private static final boolean isValidDiagonal(final Node a, final Node b) {
|
||||
return a.next.idx != b.idx && a.previous.idx != b.idx
|
||||
&& isIntersectingPolygon(a, a.getX(), a.getY(), b.getX(), b.getY()) == false
|
||||
&& isLocallyInside(a, b) && isLocallyInside(b, a)
|
||||
&& middleInsert(a, a.getX(), a.getY(), b.getX(), b.getY());
|
||||
}
|
||||
|
||||
private static final boolean isLocallyInside(final Node a, final Node b) {
|
||||
// if a is cw
|
||||
if (area(a.previous.getX(), a.previous.getY(), a.getX(), a.getY(), a.next.getX(), a.next.getY()) < 0) {
|
||||
return area(a.getX(), a.getY(), b.getX(), b.getY(), a.next.getX(), a.next.getY()) >= 0
|
||||
&& area(a.getX(), a.getY(), a.previous.getX(), a.previous.getY(), b.getX(), b.getY()) >= 0;
|
||||
}
|
||||
// ccw
|
||||
return area(a.getX(), a.getY(), b.getX(), b.getY(), a.previous.getX(), a.previous.getY()) < 0
|
||||
|| area(a.getX(), a.getY(), a.next.getX(), a.next.getY(), b.getX(), b.getY()) < 0;
|
||||
}
|
||||
|
||||
/** Determine whether the middle point of a polygon diagonal is contained within the polygon */
|
||||
private static final boolean middleInsert(final Node start, final double x0, final double y0,
|
||||
final double x1, final double y1) {
|
||||
Node node = start;
|
||||
Node nextNode;
|
||||
boolean lIsInside = false;
|
||||
final double lDx = (x0 + x1) / 2.0f;
|
||||
final double lDy = (y0 + y1) / 2.0f;
|
||||
do {
|
||||
nextNode = node.next;
|
||||
if (node.getY() > lDy != nextNode.getY() > lDy &&
|
||||
lDx < (nextNode.getX() - node.getX()) * (lDy - node.getY()) / (nextNode.getY() - node.getY()) + node.getX()) {
|
||||
lIsInside = !lIsInside;
|
||||
}
|
||||
node = node.next;
|
||||
} while (node != start);
|
||||
return lIsInside;
|
||||
}
|
||||
|
||||
/** Determines if the diagonal of a polygon is intersecting with any polygon elements. **/
|
||||
private static final boolean isIntersectingPolygon(final Node start, final double x0, final double y0,
|
||||
final double x1, final double y1) {
|
||||
Node node = start;
|
||||
Node nextNode;
|
||||
do {
|
||||
nextNode = node.next;
|
||||
if(node.getX() != x0 && node.getY() != y0 && nextNode.getX() != x0
|
||||
&& nextNode.getY() != y0 && node.getX() != x1 && node.getY() != y1
|
||||
&& nextNode.getX() != x1 && nextNode.getY() != y1) {
|
||||
return linesIntersect(node.getX(), node.getY(), nextNode.getX(), nextNode.getY(), x0, y0, x1, y1);
|
||||
}
|
||||
node = nextNode;
|
||||
} while (node != start);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Determines whether two line segments intersect. **/
|
||||
public static final boolean linesIntersect(final double aX0, final double aY0, final double aX1, final double aY1,
|
||||
final double bX0, final double bY0, final double bX1, final double bY1) {
|
||||
return (area(aX0, aY0, aX1, aY1, bX0, bY0) > 0) != (area(aX0, aY0, aX1, aY1, bX1, bY1) > 0)
|
||||
&& (area(bX0, bY0, bX1, bY1, aX0, aY0) > 0) != (area(bX0, bY0, bX1, bY1, aX1, aY1) > 0);
|
||||
}
|
||||
|
||||
/** Interlinks polygon nodes in Z-Order. **/
|
||||
private static final void sortByMorton(Node start) {
|
||||
start.previousZ.nextZ = null;
|
||||
start.previousZ = null;
|
||||
// Sort the generated ring using Z ordering.
|
||||
tathamSort(start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simon Tatham's doubly-linked list O(n log n) mergesort
|
||||
* see: http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
|
||||
**/
|
||||
private static final void tathamSort(Node list) {
|
||||
Node p, q, e, tail;
|
||||
int i, numMerges, pSize, qSize;
|
||||
int inSize = 1;
|
||||
|
||||
if (list == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
p = list;
|
||||
list = null;
|
||||
tail = null;
|
||||
// count number of merges in this pass
|
||||
numMerges = 0;
|
||||
|
||||
while(p != null) {
|
||||
++numMerges;
|
||||
// step 'insize' places along from p
|
||||
q = p;
|
||||
for (i = 0, pSize = 0; i < inSize && q != null; ++i, ++pSize, q = q.nextZ);
|
||||
// if q hasn't fallen off end, we have two lists to merge
|
||||
qSize = inSize;
|
||||
|
||||
// now we have two lists; merge
|
||||
while (pSize > 0 || (qSize > 0 && q != null)) {
|
||||
// decide whether next element of merge comes from p or q
|
||||
if (pSize == 0) {
|
||||
// p is empty; e must come from q
|
||||
e = q;
|
||||
q = q.nextZ;
|
||||
--qSize;
|
||||
} else if (qSize == 0 || q == null) {
|
||||
// q is empty; e must come from p
|
||||
e = p;
|
||||
p = p.nextZ;
|
||||
--pSize;
|
||||
} else if (Long.compareUnsigned(p.morton, q.morton) <= 0) {
|
||||
// first element of p is lower (or same); e must come from p
|
||||
e = p;
|
||||
p = p.nextZ;
|
||||
--pSize;
|
||||
} else {
|
||||
// first element of q is lower; e must come from q
|
||||
e = q;
|
||||
q = q.nextZ;
|
||||
--qSize;
|
||||
}
|
||||
|
||||
if (tail != null) {
|
||||
tail.nextZ = e;
|
||||
} else {
|
||||
list = e;
|
||||
}
|
||||
// maintain reverse pointers
|
||||
e.previousZ = tail;
|
||||
tail = e;
|
||||
}
|
||||
// now p has stepped 'insize' places along, and q has too
|
||||
p = q;
|
||||
}
|
||||
|
||||
tail.nextZ = null;
|
||||
inSize *= 2;
|
||||
} while (numMerges > 1);
|
||||
}
|
||||
|
||||
/** utility method to filter a single duplicate or colinear triangle */
|
||||
private static boolean filter(final Polygon polygon, final int i, final Node node) {
|
||||
final double x = polygon.getPolyLon(i);
|
||||
final double y = polygon.getPolyLat(i);
|
||||
final boolean equal = (x == node.getX() && y == node.getY());
|
||||
if (equal == true) {
|
||||
return true;
|
||||
} else if (node.previous == node || node.previous.previous == node) {
|
||||
return false;
|
||||
}
|
||||
return area(node.previous.previous.getX(), node.previous.previous.getY(), node.previous.getX(), node.previous.getY(), x, y) == 0d;
|
||||
}
|
||||
|
||||
/** Eliminate colinear/duplicate points from the doubly linked list */
|
||||
private static final Node filterPoints(final Node start, Node end) {
|
||||
if (start == null) {
|
||||
return start;
|
||||
}
|
||||
|
||||
if(end == null) {
|
||||
end = start;
|
||||
}
|
||||
|
||||
Node node = start;
|
||||
Node nextNode;
|
||||
Node prevNode;
|
||||
boolean continueIteration;
|
||||
|
||||
do {
|
||||
continueIteration = false;
|
||||
nextNode = node.next;
|
||||
prevNode = node.previous;
|
||||
if (node.isSteiner == false && isVertexEquals(node, nextNode)
|
||||
|| area(prevNode.getX(), prevNode.getY(), node.getX(), node.getY(), nextNode.getX(), nextNode.getY()) == 0) {
|
||||
// Remove the node
|
||||
removeNode(node);
|
||||
node = end = prevNode;
|
||||
|
||||
if (node == nextNode) {
|
||||
break;
|
||||
}
|
||||
continueIteration = true;
|
||||
} else {
|
||||
node = nextNode;
|
||||
}
|
||||
} while (continueIteration || node != end);
|
||||
return end;
|
||||
}
|
||||
|
||||
/** Creates a node and optionally links it with a previous node in a circular doubly-linked list */
|
||||
private static final Node insertNode(final Polygon polygon, int index, final Node lastNode) {
|
||||
final Node node = new Node(polygon, index);
|
||||
if(lastNode == null) {
|
||||
node.previous = node;
|
||||
node.previousZ = node;
|
||||
node.next = node;
|
||||
node.nextZ = node;
|
||||
} else {
|
||||
node.next = lastNode.next;
|
||||
node.nextZ = lastNode.nextZ;
|
||||
node.previous = lastNode;
|
||||
node.previousZ = lastNode;
|
||||
lastNode.next.previous = node;
|
||||
lastNode.nextZ.previousZ = node;
|
||||
lastNode.next = node;
|
||||
lastNode.nextZ = node;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Removes a node from the doubly linked list */
|
||||
private static final void removeNode(Node node) {
|
||||
node.next.previous = node.previous;
|
||||
node.previous.next = node.next;
|
||||
|
||||
if (node.previousZ != null) {
|
||||
node.previousZ.nextZ = node.nextZ;
|
||||
}
|
||||
if (node.nextZ != null) {
|
||||
node.nextZ.previousZ = node.previousZ;
|
||||
}
|
||||
}
|
||||
|
||||
/** Determines if two point vertices are equal. **/
|
||||
private static final boolean isVertexEquals(final Node a, final Node b) {
|
||||
return a.getX() == b.getX() && a.getY() == b.getY();
|
||||
}
|
||||
|
||||
/** Compute signed area of triangle */
|
||||
private static double area(final double aX, final double aY, final double bX, final double bY,
|
||||
final double cX, final double cY) {
|
||||
return (bY - aY) * (cX - bX) - (bX - aX) * (cY - bY);
|
||||
}
|
||||
|
||||
/** Compute whether point is in a candidate ear */
|
||||
private static boolean pointInEar(final double x, final double y, final double ax, final double ay,
|
||||
final double bx, final double by, final double cx, final double cy) {
|
||||
return (cx - x) * (ay - y) - (ax - x) * (cy - y) >= 0 &&
|
||||
(ax - x) * (by - y) - (bx - x) * (ay - y) >= 0 &&
|
||||
(bx - x) * (cy - y) - (cx - x) * (by - y) >= 0;
|
||||
}
|
||||
|
||||
/** compute whether the given x, y point is in a triangle; uses the winding order method */
|
||||
public static boolean pointInTriangle (double x, double y, double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
int a = orient(x, y, ax, ay, bx, by);
|
||||
int b = orient(x, y, bx, by, cx, cy);
|
||||
if (a == 0 || b == 0 || a < 0 == b < 0) {
|
||||
int c = orient(x, y, cx, cy, ax, ay);
|
||||
return c == 0 || (c < 0 == (b < 0 || a < 0));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Brute force compute if a point is in the polygon by traversing entire triangulation
|
||||
* todo: speed this up using either binary tree or prefix coding (filtering by bounding box of triangle)
|
||||
**/
|
||||
public static final boolean pointInPolygon(final List<Triangle> tessellation, double lat, double lon) {
|
||||
// each triangle
|
||||
for (int i = 0; i < tessellation.size(); ++i) {
|
||||
if (tessellation.get(i).containsPoint(lat, lon)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Circular Doubly-linked list used for polygon coordinates */
|
||||
protected static class Node {
|
||||
// vertex index in the polygon
|
||||
private final int idx;
|
||||
// reference to the polygon for lat/lon values
|
||||
private final Polygon polygon;
|
||||
// encoded x value
|
||||
private final int x;
|
||||
// encoded y value
|
||||
private final int y;
|
||||
// morton code for sorting
|
||||
private final long morton;
|
||||
|
||||
// previous node
|
||||
private Node previous;
|
||||
// next node
|
||||
private Node next;
|
||||
// previous z node
|
||||
private Node previousZ;
|
||||
// next z node
|
||||
private Node nextZ;
|
||||
// triangle center
|
||||
private boolean isSteiner = false;
|
||||
|
||||
protected Node(final Polygon polygon, final int index) {
|
||||
this.idx = index;
|
||||
this.polygon = polygon;
|
||||
this.y = encodeLatitude(polygon.getPolyLat(idx));
|
||||
this.x = encodeLongitude(polygon.getPolyLon(idx));
|
||||
this.morton = BitUtil.interleave(x ^ 0x80000000, y ^ 0x80000000);
|
||||
this.previous = null;
|
||||
this.next = null;
|
||||
this.previousZ = null;
|
||||
this.nextZ = null;
|
||||
}
|
||||
|
||||
/** simple deep copy constructor */
|
||||
protected Node(Node other) {
|
||||
this.idx = other.idx;
|
||||
this.polygon = other.polygon;
|
||||
this.morton = other.morton;
|
||||
this.x = other.x;
|
||||
this.y = other.y;
|
||||
this.previous = other.previous;
|
||||
this.next = other.next;
|
||||
this.previousZ = other.previousZ;
|
||||
this.nextZ = other.nextZ;
|
||||
}
|
||||
|
||||
/** get the x value */
|
||||
public final double getX() {
|
||||
return polygon.getPolyLon(idx);
|
||||
}
|
||||
|
||||
/** get the y value */
|
||||
public final double getY() {
|
||||
return polygon.getPolyLat(idx);
|
||||
}
|
||||
|
||||
/** get the longitude value */
|
||||
public final double getLon() {
|
||||
return polygon.getPolyLon(idx);
|
||||
}
|
||||
|
||||
/** get the latitude value */
|
||||
public final double getLat() {
|
||||
return polygon.getPolyLat(idx);
|
||||
}
|
||||
|
||||
/** compare nodes by y then x */
|
||||
public int compare(Node other) {
|
||||
if (this.getLat() > other.getLat()) {
|
||||
return 1;
|
||||
} else if (this.getLat() == other.getLat()) {
|
||||
if (this.getLon() > other.getLon()) {
|
||||
return 1;
|
||||
} else if (this.getLon() == other.getLon()) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (this.previous == null)
|
||||
builder.append("||-");
|
||||
else
|
||||
builder.append(this.previous.idx + " <- ");
|
||||
builder.append(this.idx);
|
||||
if (this.next == null)
|
||||
builder.append(" -||");
|
||||
else
|
||||
builder.append(" -> " + this.next.idx);
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/** Triangle in the tessellated mesh */
|
||||
public final static class Triangle {
|
||||
Node[] vertex = new Node[3];
|
||||
|
||||
protected Triangle(Node a, Node b, Node c) {
|
||||
// sort nodes by morton value
|
||||
Node tA = a;
|
||||
Node tB = b;
|
||||
Node tC = c;
|
||||
Node temp;
|
||||
if (a.compare(b) > 0) {
|
||||
temp = tA;
|
||||
tA = tB;
|
||||
tB = temp;
|
||||
}
|
||||
if (b.compare(c) > 0) {
|
||||
temp = tB;
|
||||
tB = tC;
|
||||
tC = temp;
|
||||
}
|
||||
if (a.compare(b) > 0) {
|
||||
temp = tA;
|
||||
tA = tB;
|
||||
tB = temp;
|
||||
}
|
||||
this.vertex[0] = tA;
|
||||
this.vertex[1] = tB;
|
||||
this.vertex[2] = tC;
|
||||
}
|
||||
|
||||
/** get quantized x value for the given vertex */
|
||||
public int getEncodedX(int vertex) {
|
||||
return this.vertex[vertex].x;
|
||||
}
|
||||
|
||||
/** get quantized y value for the given vertex */
|
||||
public int getEncodedY(int vertex) {
|
||||
return this.vertex[vertex].y;
|
||||
}
|
||||
|
||||
/** get latitude value for the given vertex */
|
||||
public double getLat(int vertex) {
|
||||
return this.vertex[vertex].getLat();
|
||||
}
|
||||
|
||||
/** get longitude value for the given vertex */
|
||||
public double getLon(int vertex) {
|
||||
return this.vertex[vertex].getLon();
|
||||
}
|
||||
|
||||
/** utility method to compute whether the point is in the triangle */
|
||||
protected boolean containsPoint(double lat, double lon) {
|
||||
return pointInTriangle(lon, lat,
|
||||
vertex[0].getLon(), vertex[0].getLat(),
|
||||
vertex[1].getLon(), vertex[1].getLat(),
|
||||
vertex[2].getLon(), vertex[2].getLat());
|
||||
}
|
||||
|
||||
/** pretty print the triangle vertices */
|
||||
public String toString() {
|
||||
String result = vertex[0].x + ", " + vertex[0].y + " " +
|
||||
vertex[1].x + ", " + vertex[1].y + " " +
|
||||
vertex[2].x + ", " + vertex[2].y;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<!doctype html public "-//w3c//dtd html 4.0 transitional//en">
|
||||
<!-- not a package-info.java, because we already defined this package in core/ -->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
|
||||
</head>
|
||||
<body>
|
||||
This package contains geo utility classes:
|
||||
<ul>
|
||||
<li>{@link org.apache.lucene.geo.Tessellator Tessellator} for decomposing shapes into a triangular mesh</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.lucene.document;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
|
||||
import org.apache.lucene.geo.GeoTestUtil;
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.RandomIndexWriter;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
import org.apache.lucene.util.TestUtil;
|
||||
|
||||
/** Test case for indexing polygons and querying by bounding box */
|
||||
public class TestLatLonShape extends LuceneTestCase {
|
||||
protected static String FIELDNAME = "field";
|
||||
protected void addPolygonsToDoc(String field, Document doc, Polygon polygon) {
|
||||
Field[] fields = LatLonShape.createIndexableFields(field, polygon);
|
||||
for (Field f : fields) {
|
||||
doc.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
|
||||
return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon);
|
||||
}
|
||||
|
||||
public void testRandomPolygons() throws Exception {
|
||||
long avgIdxTime = 0;
|
||||
int numVertices;
|
||||
int numPolys = RandomNumbers.randomIntBetween(random(), 50, 100);
|
||||
|
||||
Directory dir = newDirectory();
|
||||
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
|
||||
long start, end;
|
||||
|
||||
Polygon polygon;
|
||||
Document document;
|
||||
System.out.println("generating " + numPolys + " polygons");
|
||||
for (int i = 0; i < numPolys;) {
|
||||
document = new Document();
|
||||
numVertices = TestUtil.nextInt(random(), 200000, 500000);
|
||||
polygon = GeoTestUtil.createRegularPolygon(0, 0, atLeast(1000000), numVertices);
|
||||
System.out.println("adding polygon " + i);
|
||||
start = System.currentTimeMillis();
|
||||
addPolygonsToDoc(FIELDNAME, document, polygon);
|
||||
writer.addDocument(document);
|
||||
end = System.currentTimeMillis();
|
||||
avgIdxTime += ((end - start) - avgIdxTime) / ++i;
|
||||
}
|
||||
System.out.println("avg index time: " + avgIdxTime);
|
||||
|
||||
// search within 50km and verify we found our doc
|
||||
IndexReader reader = writer.getReader();
|
||||
IndexSearcher searcher = newSearcher(reader);
|
||||
start = System.currentTimeMillis();
|
||||
assertEquals(0, searcher.count(newRectQuery("field", -89.9, -89.8, -179.9, -179.8d)));
|
||||
end = System.currentTimeMillis();
|
||||
|
||||
System.out.println("search: " + (end - start));
|
||||
|
||||
reader.close();
|
||||
writer.close();
|
||||
dir.close();
|
||||
}
|
||||
|
||||
/** test we can search for a point */
|
||||
public void testBasicIntersects() throws Exception {
|
||||
Directory dir = newDirectory();
|
||||
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
|
||||
|
||||
int numVertices = TestUtil.nextInt(random(), 200000, 500000);
|
||||
|
||||
// add a random polygon without a hole
|
||||
Polygon p = GeoTestUtil.createRegularPolygon(0, 90, atLeast(1000000), numVertices);
|
||||
Document document = new Document();
|
||||
addPolygonsToDoc(FIELDNAME, document, p);
|
||||
writer.addDocument(document);
|
||||
|
||||
// add a random polygon with a hole
|
||||
Polygon inner = new Polygon(new double[] {-1d, -1d, 1d, 1d, -1d},
|
||||
new double[] {-91d, -89d, -89d, -91.0, -91.0});
|
||||
Polygon outer = GeoTestUtil.createRegularPolygon(0, -90, atLeast(1000000), numVertices);
|
||||
|
||||
document = new Document();
|
||||
addPolygonsToDoc(FIELDNAME, document, new Polygon(outer.getPolyLats(), outer.getPolyLons(), inner));
|
||||
writer.addDocument(document);
|
||||
|
||||
////// search /////
|
||||
// search an intersecting bbox
|
||||
IndexReader reader = writer.getReader();
|
||||
IndexSearcher searcher = newSearcher(reader);
|
||||
Query q = newRectQuery(FIELDNAME, -1d, 1d, p.minLon, p.maxLon);
|
||||
assertEquals(1, searcher.count(q));
|
||||
|
||||
// search a disjoint bbox
|
||||
q = newRectQuery(FIELDNAME, p.minLat-1d, p.minLat+1, p.minLon-1d, p.minLon+1d);
|
||||
assertEquals(0, searcher.count(q));
|
||||
|
||||
// search a bbox in the hole
|
||||
q = newRectQuery(FIELDNAME, inner.minLat + 1e-6, inner.maxLat - 1e-6, inner.minLon + 1e-6, inner.maxLon - 1e-6);
|
||||
assertEquals(0, searcher.count(q));
|
||||
|
||||
reader.close();
|
||||
writer.close();
|
||||
dir.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.lucene.document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.lucene.geo.GeoTestUtil;
|
||||
import org.apache.lucene.geo.Polygon;
|
||||
import org.apache.lucene.geo.Polygon2D;
|
||||
import org.apache.lucene.geo.Rectangle;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.IndexWriterConfig;
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.index.MultiDocValues;
|
||||
import org.apache.lucene.index.MultiFields;
|
||||
import org.apache.lucene.index.NumericDocValues;
|
||||
import org.apache.lucene.index.PointValues.Relation;
|
||||
import org.apache.lucene.index.SerialMergeScheduler;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.ScoreMode;
|
||||
import org.apache.lucene.search.SimpleCollector;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.util.Bits;
|
||||
import org.apache.lucene.util.FixedBitSet;
|
||||
import org.apache.lucene.util.IOUtils;
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
|
||||
|
||||
/** base Test case for {@link LatLonShape} indexing and search */
|
||||
public class TestLatLonShapeQueries extends LuceneTestCase {
|
||||
protected static final String FIELD_NAME = "shape";
|
||||
|
||||
private Polygon quantizePolygon(Polygon polygon) {
|
||||
double[] lats = new double[polygon.numPoints()];
|
||||
double[] lons = new double[polygon.numPoints()];
|
||||
for (int i = 0; i < lats.length; ++i) {
|
||||
lats[i] = quantizeLat(polygon.getPolyLat(i));
|
||||
lons[i] = quantizeLon(polygon.getPolyLon(i));
|
||||
}
|
||||
return new Polygon(lats, lons);
|
||||
}
|
||||
|
||||
protected double quantizeLat(double rawLat) {
|
||||
return decodeLatitude(encodeLatitude(rawLat));
|
||||
}
|
||||
|
||||
protected double quantizeLatCeil(double rawLat) {
|
||||
return decodeLatitude(encodeLatitudeCeil(rawLat));
|
||||
}
|
||||
|
||||
protected double quantizeLon(double rawLon) {
|
||||
return decodeLongitude(encodeLongitude(rawLon));
|
||||
}
|
||||
|
||||
protected double quantizeLonCeil(double rawLon) {
|
||||
return decodeLongitude(encodeLongitudeCeil(rawLon));
|
||||
}
|
||||
|
||||
protected void addPolygonsToDoc(String field, Document doc, Polygon polygon) {
|
||||
Field[] fields = LatLonShape.createIndexableFields(field, polygon);
|
||||
for (Field f : fields) {
|
||||
doc.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
|
||||
return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon);
|
||||
}
|
||||
|
||||
public void testRandomTiny() throws Exception {
|
||||
// Make sure single-leaf-node case is OK:
|
||||
doTestRandom(10);
|
||||
}
|
||||
|
||||
public void testRandomMedium() throws Exception {
|
||||
doTestRandom(10000);
|
||||
}
|
||||
|
||||
@Nightly
|
||||
public void testRandomBig() throws Exception {
|
||||
doTestRandom(200000);
|
||||
}
|
||||
|
||||
private void doTestRandom(int count) throws Exception {
|
||||
int numPolygons = atLeast(count);
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println("TEST: numPolygons=" + numPolygons);
|
||||
}
|
||||
|
||||
Polygon[] polygons = new Polygon[numPolygons];
|
||||
for (int id = 0; id < numPolygons; ++id) {
|
||||
int x = random().nextInt(20);
|
||||
if (x == 17) {
|
||||
polygons[id] = null;
|
||||
if (VERBOSE) {
|
||||
System.out.println(" id=" + id + " is missing");
|
||||
}
|
||||
} else {
|
||||
// create a polygon that does not cross the dateline
|
||||
polygons[id] = GeoTestUtil.nextPolygon();
|
||||
}
|
||||
}
|
||||
verify(polygons);
|
||||
}
|
||||
|
||||
private void verify(Polygon... polygons) throws Exception {
|
||||
verifyRandomBBoxes(polygons);
|
||||
}
|
||||
|
||||
protected void verifyRandomBBoxes(Polygon... polygons) throws Exception {
|
||||
IndexWriterConfig iwc = newIndexWriterConfig();
|
||||
iwc.setMergeScheduler(new SerialMergeScheduler());
|
||||
int mbd = iwc.getMaxBufferedDocs();
|
||||
if (mbd != -1 && mbd < polygons.length / 100) {
|
||||
iwc.setMaxBufferedDocs(polygons.length / 100);
|
||||
}
|
||||
Directory dir;
|
||||
if (polygons.length > 1000) {
|
||||
dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
|
||||
} else {
|
||||
dir = newDirectory();
|
||||
}
|
||||
|
||||
Set<Integer> deleted = new HashSet<>();
|
||||
IndexWriter w = new IndexWriter(dir, iwc);
|
||||
Polygon2D[] poly2D = new Polygon2D[polygons.length];
|
||||
for (int id = 0; id < polygons.length; ++id) {
|
||||
Document doc = new Document();
|
||||
doc.add(newStringField("id", "" + id, Field.Store.NO));
|
||||
doc.add(new NumericDocValuesField("id", id));
|
||||
if (polygons[id] != null) {
|
||||
try {
|
||||
addPolygonsToDoc(FIELD_NAME, doc, polygons[id]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// GeoTestUtil will occassionally create invalid polygons
|
||||
// invalid polygons will not tessellate
|
||||
// we skip those polygons that will not tessellate, relying on the TestTessellator class
|
||||
// to ensure the Tessellator correctly identified a malformed shape and its not a bug
|
||||
if (VERBOSE) {
|
||||
System.out.println(" id=" + id + " could not tessellate. Malformed shape " + polygons[id] + " detected");
|
||||
}
|
||||
// remove and skip the malformed shape
|
||||
polygons[id] = null;
|
||||
continue;
|
||||
}
|
||||
poly2D[id] = Polygon2D.create(quantizePolygon(polygons[id]));
|
||||
}
|
||||
w.addDocument(doc);
|
||||
if (id > 0 && random().nextInt(100) == 42) {
|
||||
int idToDelete = random().nextInt(id);
|
||||
w.deleteDocuments(new Term("id", ""+idToDelete));
|
||||
deleted.add(idToDelete);
|
||||
if (VERBOSE) {
|
||||
System.out.println(" delete id=" + idToDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (random().nextBoolean()) {
|
||||
w.forceMerge(1);
|
||||
}
|
||||
final IndexReader r = DirectoryReader.open(w);
|
||||
w.close();
|
||||
|
||||
IndexSearcher s = newSearcher(r);
|
||||
|
||||
final int iters = atLeast(75);
|
||||
|
||||
Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
|
||||
int maxDoc = s.getIndexReader().maxDoc();
|
||||
|
||||
for (int iter = 0; iter < iters; ++iter) {
|
||||
if (VERBOSE) {
|
||||
System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
|
||||
}
|
||||
|
||||
// BBox
|
||||
Rectangle rect = GeoTestUtil.nextBoxNotCrossingDateline();
|
||||
Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
|
||||
|
||||
if (VERBOSE) {
|
||||
System.out.println(" query=" + query);
|
||||
}
|
||||
|
||||
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(r, "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 (polygons[id] == null) {
|
||||
expected = false;
|
||||
} else {
|
||||
// check quantized poly against quantized query
|
||||
expected = poly2D[id].relate(quantizeLatCeil(rect.minLat), quantizeLat(rect.maxLat),
|
||||
quantizeLonCeil(rect.minLon), quantizeLon(rect.maxLon)) != Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
|
||||
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(" query=" + query + " docID=" + docID + "\n");
|
||||
b.append(" polygon=" + quantizePolygon(polygons[id]) + "\n");
|
||||
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
|
||||
b.append(" rect=Rectangle(" + quantizeLatCeil(rect.minLat) + " TO " + quantizeLat(rect.maxLat) + " lon=" + quantizeLonCeil(rect.minLon) + " TO " + quantizeLon(rect.maxLon) + ")");
|
||||
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");
|
||||
}
|
||||
}
|
||||
IOUtils.close(r, dir);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.lucene.geo;
|
||||
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
|
||||
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
|
||||
import static org.apache.lucene.geo.GeoTestUtil.nextBox;
|
||||
|
||||
/** Test case for the Polygon {@link Tessellator} class */
|
||||
public class TestTessellator extends LuceneTestCase {
|
||||
|
||||
public void testLinesIntersect() {
|
||||
Rectangle rect = nextBox();
|
||||
// quantize lat/lon of bounding box:
|
||||
int minX = encodeLongitude(rect.minLon);
|
||||
int maxX = encodeLongitude(rect.maxLon);
|
||||
int minY = encodeLatitude(rect.minLat);
|
||||
int maxY = encodeLatitude(rect.maxLat);
|
||||
// simple case; test intersecting diagonals
|
||||
assertTrue(Tessellator.linesIntersect(minX, minY, maxX, maxY, maxX, minY, minX, maxY));
|
||||
// test closest encoded value
|
||||
assertFalse(Tessellator.linesIntersect(minX, maxY, maxX, maxY, minX - 1, minY, minX - 1, maxY));
|
||||
}
|
||||
|
||||
public void testSimpleTessellation() throws Exception {
|
||||
Polygon poly = GeoTestUtil.createRegularPolygon(0.0, 0.0, 1000000, 1000000);
|
||||
Polygon inner = new Polygon(new double[] {-1.0, -1.0, 0.5, 1.0, 1.0, 0.5, -1.0},
|
||||
new double[]{1.0, -1.0, -0.5, -1.0, 1.0, 0.5, 1.0});
|
||||
Polygon inner2 = new Polygon(new double[] {-1.0, -1.0, 0.5, 1.0, 1.0, 0.5, -1.0},
|
||||
new double[]{-2.0, -4.0, -3.5, -4.0, -2.0, -2.5, -2.0});
|
||||
poly = new Polygon(poly.getPolyLats(), poly.getPolyLons(), inner, inner2);
|
||||
assertTrue(Tessellator.tessellate(poly).size() > 0);
|
||||
}
|
||||
}
|
|
@ -304,12 +304,12 @@ public class GeoTestUtil {
|
|||
|
||||
/** returns next pseudorandom box: can cross the 180th meridian */
|
||||
public static Rectangle nextBox() {
|
||||
return nextBoxInternal(nextLatitude(), nextLatitude(), nextLongitude(), nextLongitude(), true);
|
||||
return nextBoxInternal(true);
|
||||
}
|
||||
|
||||
/** returns next pseudorandom box: does not cross the 180th meridian */
|
||||
public static Rectangle nextBoxNotCrossingDateline() {
|
||||
return nextBoxInternal(nextLatitude(), nextLatitude(), nextLongitude(), nextLongitude(), false);
|
||||
return nextBoxInternal( false);
|
||||
}
|
||||
|
||||
/** Makes an n-gon, centered at the provided lat/lon, and each vertex approximately
|
||||
|
@ -402,7 +402,7 @@ public class GeoTestUtil {
|
|||
}
|
||||
}
|
||||
|
||||
Rectangle box = nextBoxInternal(nextLatitude(), nextLatitude(), nextLongitude(), nextLongitude(), false);
|
||||
Rectangle box = nextBoxInternal(false);
|
||||
if (random().nextBoolean()) {
|
||||
// box
|
||||
return boxPolygon(box);
|
||||
|
@ -412,7 +412,20 @@ public class GeoTestUtil {
|
|||
}
|
||||
}
|
||||
|
||||
private static Rectangle nextBoxInternal(double lat0, double lat1, double lon0, double lon1, boolean canCrossDateLine) {
|
||||
private static Rectangle nextBoxInternal(boolean canCrossDateLine) {
|
||||
// prevent lines instead of boxes
|
||||
double lat0 = nextLatitude();
|
||||
double lat1 = nextLatitude();
|
||||
while (lat0 == lat1) {
|
||||
lat1 = nextLatitude();
|
||||
}
|
||||
// prevent lines instead of boxes
|
||||
double lon0 = nextLongitude();
|
||||
double lon1 = nextLongitude();
|
||||
while (lon0 == lon1) {
|
||||
lon1 = nextLongitude();
|
||||
}
|
||||
|
||||
if (lat1 < lat0) {
|
||||
double x = lat0;
|
||||
lat0 = lat1;
|
||||
|
@ -483,21 +496,9 @@ public class GeoTestUtil {
|
|||
//System.out.println(" len=" + len);
|
||||
double lat = centerLat + len * Math.cos(SloppyMath.toRadians(angle));
|
||||
double lon = centerLon + len * Math.sin(SloppyMath.toRadians(angle));
|
||||
if (lon <= GeoUtils.MIN_LON_INCL || lon >= GeoUtils.MAX_LON_INCL) {
|
||||
// cannot cross dateline: try again!
|
||||
continue newPoly;
|
||||
}
|
||||
if (lat > 90) {
|
||||
// cross the north pole
|
||||
lat = 180 - lat;
|
||||
lon = 180 - lon;
|
||||
} else if (lat < -90) {
|
||||
// cross the south pole
|
||||
lat = -180 - lat;
|
||||
lon = 180 - lon;
|
||||
}
|
||||
if (lon <= GeoUtils.MIN_LON_INCL || lon >= GeoUtils.MAX_LON_INCL) {
|
||||
// cannot cross dateline: try again!
|
||||
if (lon <= GeoUtils.MIN_LON_INCL || lon >= GeoUtils.MAX_LON_INCL ||
|
||||
lat > 90 || lat < -90) {
|
||||
// cannot cross dateline or pole: try again!
|
||||
continue newPoly;
|
||||
}
|
||||
lats.add(lat);
|
||||
|
|
Loading…
Reference in New Issue