From 20b58f0b0ffdc8e1f0bd7c1fe99b6b5ee1c40af6 Mon Sep 17 00:00:00 2001 From: Nick Knize Date: Tue, 18 Dec 2018 16:48:30 -0600 Subject: [PATCH] [GEO] Fork Lucene's LatLonShape Classes to local lucene package (#36794) Lucene 7.6 uses a smaller encoding for LatLonShape. This commit forks the LatLonShape classes to Elasticsearch's local lucene package. These classes will be removed on the release of Lucene 7.6. --- .../forbidden/es-server-signatures.txt | 12 + .../apache/lucene/document/XLatLonShape.java | 373 ++++++++ .../XLatLonShapeBoundingBoxQuery.java | 96 ++ .../document/XLatLonShapeLineQuery.java | 134 +++ .../document/XLatLonShapePolygonQuery.java | 123 +++ .../lucene/document/XLatLonShapeQuery.java | 364 +++++++ .../org/apache/lucene/geo/XRectangle2D.java | 317 +++++++ .../org/apache/lucene/geo/XTessellator.java | 889 ++++++++++++++++++ .../common/geo/ShapeRelation.java | 2 +- .../index/mapper/GeoShapeFieldMapper.java | 22 +- .../index/query/GeoShapeQueryBuilder.java | 14 +- x-pack/plugin/sql/sql-cli/build.gradle | 5 +- 12 files changed, 2330 insertions(+), 21 deletions(-) create mode 100644 server/src/main/java/org/apache/lucene/document/XLatLonShape.java create mode 100644 server/src/main/java/org/apache/lucene/document/XLatLonShapeBoundingBoxQuery.java create mode 100644 server/src/main/java/org/apache/lucene/document/XLatLonShapeLineQuery.java create mode 100644 server/src/main/java/org/apache/lucene/document/XLatLonShapePolygonQuery.java create mode 100644 server/src/main/java/org/apache/lucene/document/XLatLonShapeQuery.java create mode 100644 server/src/main/java/org/apache/lucene/geo/XRectangle2D.java create mode 100644 server/src/main/java/org/apache/lucene/geo/XTessellator.java diff --git a/buildSrc/src/main/resources/forbidden/es-server-signatures.txt b/buildSrc/src/main/resources/forbidden/es-server-signatures.txt index d0757038c59..01c7d189073 100644 --- a/buildSrc/src/main/resources/forbidden/es-server-signatures.txt +++ b/buildSrc/src/main/resources/forbidden/es-server-signatures.txt @@ -147,3 +147,15 @@ org.apache.logging.log4j.Logger#error(java.lang.Object) org.apache.logging.log4j.Logger#error(java.lang.Object, java.lang.Throwable) org.apache.logging.log4j.Logger#fatal(java.lang.Object) org.apache.logging.log4j.Logger#fatal(java.lang.Object, java.lang.Throwable) + +# Remove once Lucene 7.7 is integrated +@defaultMessage Use org.apache.lucene.document.XLatLonShape classes instead +org.apache.lucene.document.LatLonShape +org.apache.lucene.document.LatLonShapeBoundingBoxQuery +org.apache.lucene.document.LatLonShapeLineQuery +org.apache.lucene.document.LatLonShapePolygonQuery +org.apache.lucene.document.LatLonShapeQuery + +org.apache.lucene.geo.Rectangle2D @ use @org.apache.lucene.geo.XRectangle2D instead + +org.apache.lucene.geo.Tessellator @ use @org.apache.lucene.geo.XTessellator instead \ No newline at end of file diff --git a/server/src/main/java/org/apache/lucene/document/XLatLonShape.java b/server/src/main/java/org/apache/lucene/document/XLatLonShape.java new file mode 100644 index 00000000000..4fd92f4508c --- /dev/null +++ b/server/src/main/java/org/apache/lucene/document/XLatLonShape.java @@ -0,0 +1,373 @@ +/* + * 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 org.apache.lucene.geo.GeoUtils; +import org.apache.lucene.geo.Line; +import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.XTessellator; +import org.apache.lucene.geo.XTessellator.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; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; +import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; + +/** + * An indexed shape utility class. + *

+ * {@link Polygon}'s are decomposed into a triangular mesh using the {@link XTessellator} utility class + * Each {@link Triangle} is encoded and indexed as a multi-value field. + *

+ * Finding all shapes that intersect a range (e.g., bounding box) at search time is efficient. + *

+ * This class defines static factory methods for common operations: + *

+ + * WARNING: 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 XLatLonShape { + public static final int BYTES = LatLonPoint.BYTES; + + protected static final FieldType TYPE = new FieldType(); + static { + TYPE.setDimensions(7, 4, BYTES); + TYPE.freeze(); + } + + // no instance: + private XLatLonShape() { + } + + /** create indexable fields for polygon geometry */ + public static Field[] createIndexableFields(String fieldName, Polygon polygon) { + // the lionshare of the indexing is done by the tessellator + List tessellation = XTessellator.tessellate(polygon); + List fields = new ArrayList<>(); + for (Triangle t : tessellation) { + fields.add(new LatLonTriangle(fieldName, t)); + } + return fields.toArray(new Field[fields.size()]); + } + + /** create indexable fields for line geometry */ + public static Field[] createIndexableFields(String fieldName, Line line) { + int numPoints = line.numPoints(); + Field[] fields = new Field[numPoints - 1]; + // create "flat" triangles + for (int i = 0, j = 1; j < numPoints; ++i, ++j) { + fields[i] = new LatLonTriangle(fieldName, line.getLat(i), line.getLon(i), line.getLat(j), line.getLon(j), + line.getLat(i), line.getLon(i)); + } + return fields; + } + + /** create indexable fields for point geometry */ + public static Field[] createIndexableFields(String fieldName, double lat, double lon) { + return new Field[] {new LatLonTriangle(fieldName, lat, lon, lat, lon, lat, lon)}; + } + + /** create a query to find all polygons that intersect a defined bounding box + **/ + public static Query newBoxQuery(String field, QueryRelation queryRelation, + double minLatitude, double maxLatitude, double minLongitude, double maxLongitude) { + return new XLatLonShapeBoundingBoxQuery(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 XLatLonShapeLineQuery(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 + **/ + public static Query newPolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) { + return new XLatLonShapePolygonQuery(field, queryRelation, polygons); + } + + /** polygons are decomposed into tessellated triangles using {@link XTessellator} + * these triangles are encoded and inserted as separate indexed POINT fields + */ + private static class LatLonTriangle extends Field { + + LatLonTriangle(String name, double aLat, double aLon, double bLat, double bLon, double cLat, double cLon) { + super(name, TYPE); + setTriangleValue(encodeLongitude(aLon), encodeLatitude(aLat), encodeLongitude(bLon), encodeLatitude(bLat), + encodeLongitude(cLon), encodeLatitude(cLat)); + } + + 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[7 * BYTES]; + fieldsData = new BytesRef(bytes); + } else { + bytes = ((BytesRef) fieldsData).bytes; + } + encodeTriangle(bytes, aY, aX, bY, bX, cY, cX); + } + } + + /** Query Relation Types **/ + public enum QueryRelation { + INTERSECTS, WITHIN, DISJOINT + } + + private static final int MINY_MINX_MAXY_MAXX_Y_X = 0; + private static final int MINY_MINX_Y_X_MAXY_MAXX = 1; + private static final int MAXY_MINX_Y_X_MINY_MAXX = 2; + private static final int MAXY_MINX_MINY_MAXX_Y_X = 3; + private static final int Y_MINX_MINY_X_MAXY_MAXX = 4; + private static final int Y_MINX_MINY_MAXX_MAXY_X = 5; + private static final int MAXY_MINX_MINY_X_Y_MAXX = 6; + private static final int MINY_MINX_Y_MAXX_MAXY_X = 7; + + /** + * A triangle is encoded using 6 points and an extra point with encoded information in three bits of how to reconstruct it. + * Triangles are encoded with CCW orientation and might be rotated to limit the number of possible reconstructions to 2^3. + * Reconstruction always happens from west to east. + */ + public static void encodeTriangle(byte[] bytes, int aLat, int aLon, int bLat, int bLon, int cLat, int cLon) { + assert bytes.length == 7 * BYTES; + int aX; + int bX; + int cX; + int aY; + int bY; + int cY; + //change orientation if CW + if (GeoUtils.orient(aLon, aLat, bLon, bLat, cLon, cLat) == -1) { + aX = cLon; + bX = bLon; + cX = aLon; + aY = cLat; + bY = bLat; + cY = aLat; + } else { + aX = aLon; + bX = bLon; + cX = cLon; + aY = aLat; + bY = bLat; + cY = cLat; + } + //rotate edges and place minX at the beginning + if (bX < aX || cX < aX) { + if (bX < cX) { + int tempX = aX; + int tempY = aY; + aX = bX; + aY = bY; + bX = cX; + bY = cY; + cX = tempX; + cY = tempY; + } else if (cX < aX) { + int tempX = aX; + int tempY = aY; + aX = cX; + aY = cY; + cX = bX; + cY = bY; + bX = tempX; + bY = tempY; + } + } else if (aX == bX && aX == cX) { + //degenerated case, all points with same longitude + //we need to prevent that aX is in the middle (not part of the MBS) + if (bY < aY || cY < aY) { + if (bY < cY) { + int tempX = aX; + int tempY = aY; + aX = bX; + aY = bY; + bX = cX; + bY = cY; + cX = tempX; + cY = tempY; + } else if (cY < aY) { + int tempX = aX; + int tempY = aY; + aX = cX; + aY = cY; + cX = bX; + cY = bY; + bX = tempX; + bY = tempY; + } + } + } + + int minX = aX; + int minY = StrictMath.min(aY, StrictMath.min(bY, cY)); + int maxX = StrictMath.max(aX, StrictMath.max(bX, cX)); + int maxY = StrictMath.max(aY, StrictMath.max(bY, cY)); + + int bits, x, y; + if (minY == aY) { + if (maxY == bY && maxX == bX) { + y = cY; + x = cX; + bits = MINY_MINX_MAXY_MAXX_Y_X; + } else if (maxY == cY && maxX == cX) { + y = bY; + x = bX; + bits = MINY_MINX_Y_X_MAXY_MAXX; + } else { + y = bY; + x = cX; + bits = MINY_MINX_Y_MAXX_MAXY_X; + } + } else if (maxY == aY) { + if (minY == bY && maxX == bX) { + y = cY; + x = cX; + bits = MAXY_MINX_MINY_MAXX_Y_X; + } else if (minY == cY && maxX == cX) { + y = bY; + x = bX; + bits = MAXY_MINX_Y_X_MINY_MAXX; + } else { + y = cY; + x = bX; + bits = MAXY_MINX_MINY_X_Y_MAXX; + } + } else if (maxX == bX && minY == bY) { + y = aY; + x = cX; + bits = Y_MINX_MINY_MAXX_MAXY_X; + } else if (maxX == cX && maxY == cY) { + y = aY; + x = bX; + bits = Y_MINX_MINY_X_MAXY_MAXX; + } else { + throw new IllegalArgumentException("Could not encode the provided triangle"); + } + NumericUtils.intToSortableBytes(minY, bytes, 0); + NumericUtils.intToSortableBytes(minX, bytes, BYTES); + NumericUtils.intToSortableBytes(maxY, bytes, 2 * BYTES); + NumericUtils.intToSortableBytes(maxX, bytes, 3 * BYTES); + NumericUtils.intToSortableBytes(y, bytes, 4 * BYTES); + NumericUtils.intToSortableBytes(x, bytes, 5 * BYTES); + NumericUtils.intToSortableBytes(bits, bytes, 6 * BYTES); + } + + /** + * Decode a triangle encoded by {@link XLatLonShape#encodeTriangle(byte[], int, int, int, int, int, int)}. + */ + public static void decodeTriangle(byte[] t, int[] triangle) { + assert triangle.length == 6; + int bits = NumericUtils.sortableBytesToInt(t, 6 * XLatLonShape.BYTES); + //extract the first three bits + int tCode = (((1 << 3) - 1) & (bits >> 0)); + switch (tCode) { + case MINY_MINX_MAXY_MAXX_Y_X: + triangle[0] = NumericUtils.sortableBytesToInt(t, 0 * XLatLonShape.BYTES); + triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * XLatLonShape.BYTES); + triangle[2] = NumericUtils.sortableBytesToInt(t, 2 * XLatLonShape.BYTES); + triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * XLatLonShape.BYTES); + triangle[4] = NumericUtils.sortableBytesToInt(t, 4 * XLatLonShape.BYTES); + triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * XLatLonShape.BYTES); + break; + case MINY_MINX_Y_X_MAXY_MAXX: + triangle[0] = NumericUtils.sortableBytesToInt(t, 0 * XLatLonShape.BYTES); + triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * XLatLonShape.BYTES); + triangle[2] = NumericUtils.sortableBytesToInt(t, 4 * XLatLonShape.BYTES); + triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * XLatLonShape.BYTES); + triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * XLatLonShape.BYTES); + triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * XLatLonShape.BYTES); + break; + case MAXY_MINX_Y_X_MINY_MAXX: + triangle[0] = NumericUtils.sortableBytesToInt(t, 2 * XLatLonShape.BYTES); + triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * XLatLonShape.BYTES); + triangle[2] = NumericUtils.sortableBytesToInt(t, 4 * XLatLonShape.BYTES); + triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * XLatLonShape.BYTES); + triangle[4] = NumericUtils.sortableBytesToInt(t, 0 * XLatLonShape.BYTES); + triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * XLatLonShape.BYTES); + break; + case MAXY_MINX_MINY_MAXX_Y_X: + triangle[0] = NumericUtils.sortableBytesToInt(t, 2 * XLatLonShape.BYTES); + triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * XLatLonShape.BYTES); + triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * XLatLonShape.BYTES); + triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * XLatLonShape.BYTES); + triangle[4] = NumericUtils.sortableBytesToInt(t, 4 * XLatLonShape.BYTES); + triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * XLatLonShape.BYTES); + break; + case Y_MINX_MINY_X_MAXY_MAXX: + triangle[0] = NumericUtils.sortableBytesToInt(t, 4 * XLatLonShape.BYTES); + triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * XLatLonShape.BYTES); + triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * XLatLonShape.BYTES); + triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * XLatLonShape.BYTES); + triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * XLatLonShape.BYTES); + triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * XLatLonShape.BYTES); + break; + case Y_MINX_MINY_MAXX_MAXY_X: + triangle[0] = NumericUtils.sortableBytesToInt(t, 4 * XLatLonShape.BYTES); + triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * XLatLonShape.BYTES); + triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * XLatLonShape.BYTES); + triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * XLatLonShape.BYTES); + triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * XLatLonShape.BYTES); + triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * XLatLonShape.BYTES); + break; + case MAXY_MINX_MINY_X_Y_MAXX: + triangle[0] = NumericUtils.sortableBytesToInt(t, 2 * XLatLonShape.BYTES); + triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * XLatLonShape.BYTES); + triangle[2] = NumericUtils.sortableBytesToInt(t, 0 * XLatLonShape.BYTES); + triangle[3] = NumericUtils.sortableBytesToInt(t, 5 * XLatLonShape.BYTES); + triangle[4] = NumericUtils.sortableBytesToInt(t, 4 * XLatLonShape.BYTES); + triangle[5] = NumericUtils.sortableBytesToInt(t, 3 * XLatLonShape.BYTES); + break; + case MINY_MINX_Y_MAXX_MAXY_X: + triangle[0] = NumericUtils.sortableBytesToInt(t, 0 * XLatLonShape.BYTES); + triangle[1] = NumericUtils.sortableBytesToInt(t, 1 * XLatLonShape.BYTES); + triangle[2] = NumericUtils.sortableBytesToInt(t, 4 * XLatLonShape.BYTES); + triangle[3] = NumericUtils.sortableBytesToInt(t, 3 * XLatLonShape.BYTES); + triangle[4] = NumericUtils.sortableBytesToInt(t, 2 * XLatLonShape.BYTES); + triangle[5] = NumericUtils.sortableBytesToInt(t, 5 * XLatLonShape.BYTES); + break; + default: + throw new IllegalArgumentException("Could not decode the provided triangle"); + } + //Points of the decoded triangle must be co-planar or CCW oriented + assert GeoUtils.orient(triangle[1], triangle[0], triangle[3], triangle[2], triangle[5], triangle[4]) >= 0; + } +} diff --git a/server/src/main/java/org/apache/lucene/document/XLatLonShapeBoundingBoxQuery.java b/server/src/main/java/org/apache/lucene/document/XLatLonShapeBoundingBoxQuery.java new file mode 100644 index 00000000000..bcf664719b7 --- /dev/null +++ b/server/src/main/java/org/apache/lucene/document/XLatLonShapeBoundingBoxQuery.java @@ -0,0 +1,96 @@ +/* + * 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 org.apache.lucene.geo.Rectangle; +import org.apache.lucene.geo.XRectangle2D; +import org.apache.lucene.index.PointValues.Relation; + +/** + * Finds all previously indexed shapes that intersect the specified bounding box. + * + *

The field must be indexed using + * {@link XLatLonShape#createIndexableFields} added per document. + * + * @lucene.experimental + **/ +final class XLatLonShapeBoundingBoxQuery extends XLatLonShapeQuery { + final XRectangle2D rectangle2D; + + XLatLonShapeBoundingBoxQuery(String field, XLatLonShape.QueryRelation queryRelation, + double minLat, double maxLat, double minLon, double maxLon) { + super(field, queryRelation); + Rectangle rectangle = new Rectangle(minLat, maxLat, minLon, maxLon); + this.rectangle2D = XRectangle2D.create(rectangle); + } + + @Override + protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle) { + return rectangle2D.relateRangeBBox(minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle); + } + + /** returns true if the query matches the encoded triangle */ + @Override + protected boolean queryMatches(byte[] t, int[] scratchTriangle) { + // decode indexed triangle + XLatLonShape.decodeTriangle(t, scratchTriangle); + + int aY = scratchTriangle[0]; + int aX = scratchTriangle[1]; + int bY = scratchTriangle[2]; + int bX = scratchTriangle[3]; + int cY = scratchTriangle[4]; + int cX = scratchTriangle[5]; + + if (queryRelation == XLatLonShape.QueryRelation.WITHIN) { + return rectangle2D.containsTriangle(aX, aY, bX, bY, cX, cY); + } + return rectangle2D.intersectsTriangle(aX, aY, bX, bY, cX, cY); + } + + @Override + public boolean equals(Object o) { + return sameClassAs(o) && equalsTo(getClass().cast(o)); + } + + @Override + protected boolean equalsTo(Object o) { + return super.equalsTo(o) && rectangle2D.equals(((XLatLonShapeBoundingBoxQuery)o).rectangle2D); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 31 * hash + rectangle2D.hashCode(); + 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(rectangle2D.toString()); + return sb.toString(); + } +} diff --git a/server/src/main/java/org/apache/lucene/document/XLatLonShapeLineQuery.java b/server/src/main/java/org/apache/lucene/document/XLatLonShapeLineQuery.java new file mode 100644 index 00000000000..90905e0d83f --- /dev/null +++ b/server/src/main/java/org/apache/lucene/document/XLatLonShapeLineQuery.java @@ -0,0 +1,134 @@ +/* + * 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 org.apache.lucene.document.XLatLonShape.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; + +import java.util.Arrays; + +/** + * Finds all previously indexed shapes that intersect the specified arbitrary {@code Line}. + *

+ * Note: + *

    + *
  • {@code QueryRelation.WITHIN} queries are not yet supported
  • + *
  • Dateline crossing is not yet supported
  • + *
+ *

+ * todo: + *

    + *
  • Add distance support for buffered queries
  • + *
+ *

The field must be indexed using + * {@link XLatLonShape#createIndexableFields} added per document. + * + * @lucene.experimental + **/ +final class XLatLonShapeLineQuery extends XLatLonShapeQuery { + final Line[] lines; + private final Line2D line2D; + + XLatLonShapeLineQuery(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(NumericUtils.sortableBytesToInt(minTriangle, minYOffset)); + double minLon = GeoEncodingUtils.decodeLongitude(NumericUtils.sortableBytesToInt(minTriangle, minXOffset)); + double maxLat = GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(maxTriangle, maxYOffset)); + double maxLon = GeoEncodingUtils.decodeLongitude(NumericUtils.sortableBytesToInt(maxTriangle, maxXOffset)); + + // check internal node against query + return line2D.relate(minLat, maxLat, minLon, maxLon); + } + + @Override + protected boolean queryMatches(byte[] t, int[] scratchTriangle) { + XLatLonShape.decodeTriangle(t, scratchTriangle); + + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle[0]); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle[1]); + double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle[2]); + double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle[3]); + double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle[4]); + double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle[5]); + + if (queryRelation == XLatLonShape.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 + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + protected boolean equalsTo(Object o) { + return super.equalsTo(o) && Arrays.equals(lines, ((XLatLonShapeLineQuery)o).lines); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 31 * hash + Arrays.hashCode(lines); + return hash; + } +} diff --git a/server/src/main/java/org/apache/lucene/document/XLatLonShapePolygonQuery.java b/server/src/main/java/org/apache/lucene/document/XLatLonShapePolygonQuery.java new file mode 100644 index 00000000000..5e97828aae2 --- /dev/null +++ b/server/src/main/java/org/apache/lucene/document/XLatLonShapePolygonQuery.java @@ -0,0 +1,123 @@ +/* + * 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 org.apache.lucene.document.XLatLonShape.QueryRelation; +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.Polygon; +import org.apache.lucene.geo.Polygon2D; +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.NumericUtils; + +import java.util.Arrays; + +/** + * Finds all previously indexed shapes that intersect the specified arbitrary. + * + *

The field must be indexed using + * {@link XLatLonShape#createIndexableFields} added per document. + * + * @lucene.experimental + **/ +final class XLatLonShapePolygonQuery extends XLatLonShapeQuery { + final Polygon[] polygons; + private final Polygon2D poly2D; + + /** + * Creates a query that matches all indexed shapes to the provided polygons + */ + XLatLonShapePolygonQuery(String field, QueryRelation queryRelation, Polygon... polygons) { + super(field, queryRelation); + if (polygons == null) { + throw new IllegalArgumentException("polygons must not be null"); + } + if (polygons.length == 0) { + throw new IllegalArgumentException("polygons must not be empty"); + } + for (int i = 0; i < polygons.length; i++) { + if (polygons[i] == null) { + throw new IllegalArgumentException("polygon[" + i + "] must not be null"); + } else if (polygons[i].minLon > polygons[i].maxLon) { + throw new IllegalArgumentException("LatLonShapePolygonQuery does not currently support querying across dateline."); + } + } + this.polygons = polygons.clone(); + this.poly2D = Polygon2D.create(polygons); + } + + @Override + protected Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle) { + + double minLat = GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(minTriangle, minYOffset)); + double minLon = GeoEncodingUtils.decodeLongitude(NumericUtils.sortableBytesToInt(minTriangle, minXOffset)); + double maxLat = GeoEncodingUtils.decodeLatitude(NumericUtils.sortableBytesToInt(maxTriangle, maxYOffset)); + double maxLon = GeoEncodingUtils.decodeLongitude(NumericUtils.sortableBytesToInt(maxTriangle, maxXOffset)); + + // check internal node against query + return poly2D.relate(minLat, maxLat, minLon, maxLon); + } + + @Override + protected boolean queryMatches(byte[] t, int[] scratchTriangle) { + XLatLonShape.decodeTriangle(t, scratchTriangle); + + double alat = GeoEncodingUtils.decodeLatitude(scratchTriangle[0]); + double alon = GeoEncodingUtils.decodeLongitude(scratchTriangle[1]); + double blat = GeoEncodingUtils.decodeLatitude(scratchTriangle[2]); + double blon = GeoEncodingUtils.decodeLongitude(scratchTriangle[3]); + double clat = GeoEncodingUtils.decodeLatitude(scratchTriangle[4]); + double clon = GeoEncodingUtils.decodeLongitude(scratchTriangle[5]); + + if (queryRelation == QueryRelation.WITHIN) { + return poly2D.relateTriangle(alon, alat, blon, blat, clon, clat) == Relation.CELL_INSIDE_QUERY; + } + // INTERSECTS + return poly2D.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("Polygon(" + polygons[0].toGeoJSON() + ")"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + protected boolean equalsTo(Object o) { + return super.equalsTo(o) && Arrays.equals(polygons, ((XLatLonShapePolygonQuery)o).polygons); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 31 * hash + Arrays.hashCode(polygons); + return hash; + } +} diff --git a/server/src/main/java/org/apache/lucene/document/XLatLonShapeQuery.java b/server/src/main/java/org/apache/lucene/document/XLatLonShapeQuery.java new file mode 100644 index 00000000000..7aded1337e4 --- /dev/null +++ b/server/src/main/java/org/apache/lucene/document/XLatLonShapeQuery.java @@ -0,0 +1,364 @@ +/* + * 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 org.apache.lucene.document.XLatLonShape.QueryRelation; +import org.apache.lucene.index.FieldInfo; +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 java.io.IOException; +import java.util.Objects; + +/** + * Base LatLonShape Query class providing common query logic for + * {@link XLatLonShapeBoundingBoxQuery} and {@link XLatLonShapePolygonQuery} + * + * Note: this class implements the majority of the INTERSECTS, WITHIN, DISJOINT relation logic + * + * @lucene.experimental + **/ +abstract class XLatLonShapeQuery extends Query { + /** field name */ + final String field; + /** query relation + * disjoint: {@code CELL_OUTSIDE_QUERY} + * intersects: {@code CELL_CROSSES_QUERY}, + * within: {@code CELL_WITHIN_QUERY} */ + final XLatLonShape.QueryRelation queryRelation; + + protected XLatLonShapeQuery(String field, final QueryRelation queryType) { + if (field == null) { + throw new IllegalArgumentException("field must not be null"); + } + this.field = field; + this.queryRelation = queryType; + } + + /** + * relates an internal node (bounding box of a range of triangles) to the target query + * Note: logic is specific to query type + * see {@link XLatLonShapeBoundingBoxQuery#relateRangeToQuery} and {@link XLatLonShapePolygonQuery#relateRangeToQuery} + */ + protected abstract Relation relateRangeBBoxToQuery(int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle); + + /** returns true if the provided triangle matches the query */ + protected abstract boolean queryMatches(byte[] triangle, int[] scratchTriangle); + + /** relates a range of triangles (internal node) to the query */ + protected Relation relateRangeToQuery(byte[] minTriangle, byte[] maxTriangle) { + // compute bounding box of internal node + Relation r = relateRangeBBoxToQuery(XLatLonShape.BYTES, 0, minTriangle, 3 * XLatLonShape.BYTES, + 2 * XLatLonShape.BYTES, maxTriangle); + if (queryRelation == QueryRelation.DISJOINT) { + return transposeRelation(r); + } + return r; + } + + @Override + public final Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException { + + return new ConstantScoreWeight(this, boost) { + + /** create a visitor that adds documents that match the query using a sparse bitset. (Used by INTERSECT) */ + protected IntersectVisitor getSparseIntersectVisitor(DocIdSetBuilder result) { + return new IntersectVisitor() { + final int[] scratchTriangle = new int[6]; + 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 (queryMatches(t, scratchTriangle)) { + adder.add(docID); + } + } + + @Override + public Relation compare(byte[] minTriangle, byte[] maxTriangle) { + return relateRangeToQuery(minTriangle, maxTriangle); + } + }; + } + + /** create a visitor that adds documents that match the query using a dense bitset. (Used by WITHIN, DISJOINT) */ + protected IntersectVisitor getDenseIntersectVisitor(FixedBitSet intersect, FixedBitSet disjoint) { + return new IntersectVisitor() { + final int[] scratchTriangle = new int[6]; + @Override + public void visit(int docID) throws IOException { + if (queryRelation == QueryRelation.DISJOINT) { + // if DISJOINT query set the doc in the disjoint bitset + disjoint.set(docID); + } else { + // for INTERSECT, and WITHIN queries we set the intersect bitset + intersect.set(docID); + } + } + + @Override + public void visit(int docID, byte[] t) throws IOException { + if (queryMatches(t, scratchTriangle)) { + intersect.set(docID); + } else { + disjoint.set(docID); + } + } + + @Override + public Relation compare(byte[] minTriangle, byte[] maxTriangle) { + return relateRangeToQuery(minTriangle, maxTriangle); + } + }; + } + + /** get a scorer supplier for INTERSECT queries */ + protected ScorerSupplier getIntersectScorerSupplier(LeafReader reader, PointValues values, Weight weight, + ScoreMode scoreMode) throws IOException { + DocIdSetBuilder result = new DocIdSetBuilder(reader.maxDoc(), values, field); + IntersectVisitor visitor = getSparseIntersectVisitor(result); + return new RelationScorerSupplier(values, visitor) { + @Override + public Scorer get(long leadCost) throws IOException { + return getIntersectsScorer(XLatLonShapeQuery.this, reader, weight, result, score(), scoreMode); + } + }; + } + + /** get a scorer supplier for all other queries (DISJOINT, WITHIN) */ + protected ScorerSupplier getScorerSupplier(LeafReader reader, PointValues values, Weight weight, + ScoreMode scoreMode) throws IOException { + if (queryRelation == QueryRelation.INTERSECTS) { + return getIntersectScorerSupplier(reader, values, weight, scoreMode); + } + + FixedBitSet intersect = new FixedBitSet(reader.maxDoc()); + FixedBitSet disjoint = new FixedBitSet(reader.maxDoc()); + IntersectVisitor visitor = getDenseIntersectVisitor(intersect, disjoint); + return new RelationScorerSupplier(values, visitor) { + @Override + public Scorer get(long leadCost) throws IOException { + return getScorer(XLatLonShapeQuery.this, weight, intersect, disjoint, score(), scoreMode); + } + }; + } + + @Override + public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException { + LeafReader reader = context.reader(); + PointValues values = reader.getPointValues(field); + if (values == null) { + // No docs in this segment had any points fields + return null; + } + FieldInfo fieldInfo = reader.getFieldInfos().fieldInfo(field); + if (fieldInfo == null) { + // No docs in this segment indexed this field at all + 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(), scoreMode, DocIdSetIterator.all(reader.maxDoc())); + } + + @Override + public long cost() { + return reader.maxDoc(); + } + }; + } else { + return getScorerSupplier(reader, values, weight, scoreMode); + } + } + + @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; + } + }; + } + + /** returns the field name */ + public String getField() { + return field; + } + + /** returns the query relation */ + public QueryRelation getQueryRelation() { + return queryRelation; + } + + @Override + public int hashCode() { + int hash = classHash(); + hash = 31 * hash + field.hashCode(); + hash = 31 * hash + queryRelation.hashCode(); + return hash; + } + + @Override + public boolean equals(Object o) { + return sameClassAs(o) && equalsTo(o); + } + + protected boolean equalsTo(Object o) { + return Objects.equals(field, ((XLatLonShapeQuery)o).field) && this.queryRelation == ((XLatLonShapeQuery)o).queryRelation; + } + + /** transpose the relation; INSIDE becomes OUTSIDE, OUTSIDE becomes INSIDE, CROSSES remains unchanged */ + private static Relation transposeRelation(Relation r) { + if (r == Relation.CELL_INSIDE_QUERY) { + return Relation.CELL_OUTSIDE_QUERY; + } else if (r == Relation.CELL_OUTSIDE_QUERY) { + return Relation.CELL_INSIDE_QUERY; + } + return Relation.CELL_CROSSES_QUERY; + } + + /** utility class for implementing constant score logic specific to INTERSECT, WITHIN, and DISJOINT */ + private abstract static class RelationScorerSupplier extends ScorerSupplier { + PointValues values; + IntersectVisitor visitor; + long cost = -1; + + RelationScorerSupplier(PointValues values, IntersectVisitor visitor) { + this.values = values; + this.visitor = visitor; + } + + /** create a visitor that clears documents that do NOT match the polygon query; used with INTERSECTS */ + private IntersectVisitor getInverseIntersectVisitor(XLatLonShapeQuery query, FixedBitSet result, int[] cost) { + return new IntersectVisitor() { + int[] scratchTriangle = new int[6]; + @Override + public void visit(int docID) { + result.clear(docID); + cost[0]--; + } + + @Override + public void visit(int docID, byte[] packedTriangle) { + if (query.queryMatches(packedTriangle, scratchTriangle) == false) { + result.clear(docID); + cost[0]--; + } + } + + @Override + public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { + return transposeRelation(query.relateRangeToQuery(minPackedValue, maxPackedValue)); + } + }; + } + + /** returns a Scorer for INTERSECT queries that uses a sparse bitset */ + protected Scorer getIntersectsScorer(XLatLonShapeQuery query, LeafReader reader, Weight weight, + DocIdSetBuilder docIdSetBuilder, final float boost, + ScoreMode scoreMode) 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(query, result, cost)); + final DocIdSetIterator iterator = new BitSetIterator(result, cost[0]); + return new ConstantScoreScorer(weight, boost, scoreMode, iterator); + } + + values.intersect(visitor); + DocIdSetIterator iterator = docIdSetBuilder.build().iterator(); + return new ConstantScoreScorer(weight, boost, scoreMode, iterator); + } + + /** returns a Scorer for all other (non INTERSECT) queries */ + protected Scorer getScorer(XLatLonShapeQuery query, Weight weight, + FixedBitSet intersect, FixedBitSet disjoint, final float boost, + ScoreMode scoreMode) throws IOException { + values.intersect(visitor); + DocIdSetIterator iterator; + if (query.queryRelation == QueryRelation.DISJOINT) { + disjoint.andNot(intersect); + iterator = new BitSetIterator(disjoint, cost()); + } else if (query.queryRelation == QueryRelation.WITHIN) { + intersect.andNot(disjoint); + iterator = new BitSetIterator(intersect, cost()); + } else { + iterator = new BitSetIterator(intersect, cost()); + } + return new ConstantScoreScorer(weight, boost, scoreMode, 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; + } + } +} diff --git a/server/src/main/java/org/apache/lucene/geo/XRectangle2D.java b/server/src/main/java/org/apache/lucene/geo/XRectangle2D.java new file mode 100644 index 00000000000..af06d0a0e39 --- /dev/null +++ b/server/src/main/java/org/apache/lucene/geo/XRectangle2D.java @@ -0,0 +1,317 @@ +/* + * 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.document.XLatLonShape; +import org.apache.lucene.index.PointValues; +import org.apache.lucene.util.FutureArrays; +import org.apache.lucene.util.NumericUtils; + +import java.util.Arrays; + +import static org.apache.lucene.document.XLatLonShape.BYTES; +import static org.apache.lucene.geo.GeoEncodingUtils.MAX_LON_ENCODED; +import static org.apache.lucene.geo.GeoEncodingUtils.MIN_LON_ENCODED; +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; +import static org.apache.lucene.geo.GeoUtils.orient; + +/** + * 2D rectangle implementation containing spatial logic. + * + * @lucene.internal + */ +public class XRectangle2D { + final byte[] bbox; + final byte[] west; + final int minX; + final int maxX; + final int minY; + final int maxY; + + private XRectangle2D(double minLat, double maxLat, double minLon, double maxLon) { + this.bbox = new byte[4 * BYTES]; + int minXenc = encodeLongitudeCeil(minLon); + int maxXenc = encodeLongitude(maxLon); + int minYenc = encodeLatitudeCeil(minLat); + int maxYenc = encodeLatitude(maxLat); + if (minYenc > maxYenc) { + minYenc = maxYenc; + } + this.minY = minYenc; + this.maxY = maxYenc; + + if (minLon > maxLon == true) { + // crossing dateline is split into east/west boxes + this.west = new byte[4 * BYTES]; + this.minX = minXenc; + this.maxX = maxXenc; + encode(MIN_LON_ENCODED, this.maxX, this.minY, this.maxY, this.west); + encode(this.minX, MAX_LON_ENCODED, this.minY, this.maxY, this.bbox); + } else { + // encodeLongitudeCeil may cause minX to be > maxX iff + // the delta between the longitude < the encoding resolution + if (minXenc > maxXenc) { + minXenc = maxXenc; + } + this.west = null; + this.minX = minXenc; + this.maxX = maxXenc; + encode(this.minX, this.maxX, this.minY, this.maxY, bbox); + } + } + + /** Builds a XRectangle2D from rectangle */ + public static XRectangle2D create(Rectangle rectangle) { + return new XRectangle2D(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon); + } + + public boolean crossesDateline() { + return minX > maxX; + } + + /** Checks if the rectangle contains the provided point **/ + public boolean queryContainsPoint(int x, int y) { + if (this.crossesDateline() == true) { + return bboxContainsPoint(x, y, MIN_LON_ENCODED, this.maxX, this.minY, this.maxY) + || bboxContainsPoint(x, y, this.minX, MAX_LON_ENCODED, this.minY, this.maxY); + } + return bboxContainsPoint(x, y, this.minX, this.maxX, this.minY, this.maxY); + } + + /** compare this to a provided rangle bounding box **/ + public PointValues.Relation relateRangeBBox(int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle) { + PointValues.Relation eastRelation = compareBBoxToRangeBBox(this.bbox, minXOffset, minYOffset, minTriangle, + maxXOffset, maxYOffset, maxTriangle); + if (this.crossesDateline() && eastRelation == PointValues.Relation.CELL_OUTSIDE_QUERY) { + return compareBBoxToRangeBBox(this.west, minXOffset, minYOffset, minTriangle, maxXOffset, maxYOffset, maxTriangle); + } + return eastRelation; + } + + /** Checks if the rectangle intersects the provided triangle **/ + public boolean intersectsTriangle(int aX, int aY, int bX, int bY, int cX, int cY) { + // 1. query contains any triangle points + 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); + int tMaxY = StrictMath.max(StrictMath.max(aY, bY), cY); + + // 2. check bounding boxes are disjoint + if (this.crossesDateline() == true) { + if (boxesAreDisjoint(tMinX, tMaxX, tMinY, tMaxY, MIN_LON_ENCODED, this.maxX, this.minY, this.maxY) + && boxesAreDisjoint(tMinX, tMaxX, tMinY, tMaxY, this.minX, MAX_LON_ENCODED, this.minY, this.maxY)) { + return false; + } + } else if (tMaxX < minX || tMinX > maxX || tMinY > maxY || tMaxY < minY) { + return false; + } + + // 3. check triangle contains any query points + if (XTessellator.pointInTriangle(minX, minY, aX, aY, bX, bY, cX, cY)) { + return true; + } else if (XTessellator.pointInTriangle(maxX, minY, aX, aY, bX, bY, cX, cY)) { + return true; + } else if (XTessellator.pointInTriangle(maxX, maxY, aX, aY, bX, bY, cX, cY)) { + return true; + } else if (XTessellator.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; + } + + /** Checks if the rectangle contains the provided triangle **/ + public boolean containsTriangle(int ax, int ay, int bx, int by, int cx, int cy) { + if (this.crossesDateline() == true) { + return bboxContainsTriangle(ax, ay, bx, by, cx, cy, MIN_LON_ENCODED, this.maxX, this.minY, this.maxY) + || bboxContainsTriangle(ax, ay, bx, by, cx, cy, this.minX, MAX_LON_ENCODED, this.minY, this.maxY); + } + return bboxContainsTriangle(ax, ay, bx, by, cx, cy, minX, maxX, minY, maxY); + } + + /** static utility method to compare a bbox with a range of triangles (just the bbox of the triangle collection) */ + private static PointValues.Relation compareBBoxToRangeBBox(final byte[] bbox, + int minXOffset, int minYOffset, byte[] minTriangle, + int maxXOffset, int maxYOffset, byte[] maxTriangle) { + // check bounding box (DISJOINT) + if (FutureArrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) > 0 || + FutureArrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, BYTES, 2 * BYTES) < 0 || + FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) > 0 || + FutureArrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 0, BYTES) < 0) { + return PointValues.Relation.CELL_OUTSIDE_QUERY; + } + + if (FutureArrays.compareUnsigned(minTriangle, minXOffset, minXOffset + BYTES, bbox, BYTES, 2 * BYTES) >= 0 && + FutureArrays.compareUnsigned(maxTriangle, maxXOffset, maxXOffset + BYTES, bbox, 3 * BYTES, 4 * BYTES) <= 0 && + FutureArrays.compareUnsigned(minTriangle, minYOffset, minYOffset + BYTES, bbox, 0, BYTES) >= 0 && + FutureArrays.compareUnsigned(maxTriangle, maxYOffset, maxYOffset + BYTES, bbox, 2 * BYTES, 3 * BYTES) <= 0) { + return PointValues.Relation.CELL_INSIDE_QUERY; + } + return PointValues.Relation.CELL_CROSSES_QUERY; + } + + /** + * encodes a bounding box into the provided byte array + */ + private static void encode(final int minX, final int maxX, final int minY, final int maxY, byte[] b) { + if (b == null) { + b = new byte[4 * XLatLonShape.BYTES]; + } + NumericUtils.intToSortableBytes(minY, b, 0); + NumericUtils.intToSortableBytes(minX, b, BYTES); + NumericUtils.intToSortableBytes(maxY, b, 2 * BYTES); + NumericUtils.intToSortableBytes(maxX, b, 3 * BYTES); + } + + /** 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 + if (edgeIntersectsQuery(ax, ay, bx, by) || + edgeIntersectsQuery(bx, by, cx, cy) || + edgeIntersectsQuery(cx, cy, ax, ay)) { + return true; + } + 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) { + if (this.crossesDateline() == true) { + return edgeIntersectsBox(ax, ay, bx, by, MIN_LON_ENCODED, this.maxX, this.minY, this.maxY) + || edgeIntersectsBox(ax, ay, bx, by, this.minX, MAX_LON_ENCODED, this.minY, this.maxY); + } + return edgeIntersectsBox(ax, ay, bx, by, this.minX, this.maxX, this.minY, this.maxY); + } + + /** 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; + } + + /** 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); + } + + /** returns true if the edge (defined by (ax, ay) (bx, by)) intersects the query */ + 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) { + return true; + } + + // right + if (orient(ax, ay, bx, by, maxX, maxY) * orient(ax, ay, bx, by, maxX, minY) <= 0 && + orient(maxX, maxY, maxX, minY, ax, ay) * orient(maxX, maxY, maxX, minY, bx, by) <= 0) { + return true; + } + + // bottom + if (orient(ax, ay, bx, by, maxX, minY) * orient(ax, ay, bx, by, minX, minY) <= 0 && + orient(maxX, minY, minX, minY, ax, ay) * orient(maxX, minY, minX, minY, bx, by) <= 0) { + return true; + } + + // left + if (orient(ax, ay, bx, by, minX, minY) * orient(ax, ay, bx, by, minX, maxY) <= 0 && + orient(minX, minY, minX, maxY, ax, ay) * orient(minX, minY, minX, maxY, bx, by) <= 0) { + return true; + } + return false; + } + + /** utility method to check if two boxes are disjoint */ + private 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 Arrays.equals(bbox, ((XRectangle2D)o).bbox) + && Arrays.equals(west, ((XRectangle2D)o).west); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 31 * hash + Arrays.hashCode(bbox); + hash = 31 * hash + Arrays.hashCode(west); + return hash; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + 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)); + if (maxX < minX) { + sb.append(" [crosses dateline!]"); + } + sb.append(")"); + return sb.toString(); + } +} diff --git a/server/src/main/java/org/apache/lucene/geo/XTessellator.java b/server/src/main/java/org/apache/lucene/geo/XTessellator.java new file mode 100644 index 00000000000..416b501202b --- /dev/null +++ b/server/src/main/java/org/apache/lucene/geo/XTessellator.java @@ -0,0 +1,889 @@ +/* + * 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. + *

+ * 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. + *

+ * Notes: + *

    + *
  • Requires valid polygons: + *
      + *
    • No self intersections + *
    • Holes may only touch at one vertex + *
    • Polygon must have an area (e.g., no "line" boxes) + *
    • sensitive to overflow (e.g, subatomic values such as E-200 can cause unexpected behavior) + *
    + *
+ *

+ * The code is a modified version of the javascript implementation provided by MapBox + * under the following license: + *

+ * ISC License + *

+ * Copyright (c) 2016, Mapbox + *

+ * 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. + *

+ * 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 + */ +public final class XTessellator { + // 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 XTessellator() {} + + /** Produces an array of vertices representing the triangulated result set of the Points array */ + public static List 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, 0, WindingOrder.CW); + // 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 XTessellator!"); + } + + // 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 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 Node createDoublyLinkedList(final Polygon polygon, int startIndex, 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) { + lastNode = insertNode(polygon, startIndex++, i, lastNode); + } + } else { + for (int i = polygon.numPoints() - 1; i >= 0; --i) { + lastNode = insertNode(polygon, startIndex++, 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 filterPoints(lastNode, null); + } + + /** Links every hole into the outer loop, producing a single-ring polygon without holes. **/ + private static Node eliminateHoles(final Polygon polygon, Node outerNode) { + // Define a list to hole a reference to each filtered hole list. + final List holeList = new ArrayList<>(); + // Iterate through each array of hole vertices. + Polygon[] holes = polygon.getHoles(); + int nodeIndex = polygon.numPoints(); + for(int i = 0; i < polygon.numHoles(); ++i) { + // create the doubly-linked hole list + Node list = createDoublyLinkedList(holes[i], nodeIndex, WindingOrder.CCW); + 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)); + } + nodeIndex += holes[i].numPoints(); + } + + // 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 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 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() && p.next.getY() != p.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; + if (x == hx) { + if (hy == p.getY()) return p; + if (hy == p.next.getY()) return p.next; + } + connection = p.getX() < p.next.getX() ? p : p.next; + } + } + p = p.next; + } while (p != outerNode); + } + + if (connection == null) { + return null; + } else if (hx == qx) { + 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 && hx != p.getX() + && pointInEar(p.getX(), p.getY(), hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy)) { + tan = Math.abs(hy - p.getY()) / (hx - p.getX()); // 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 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 List earcutLinkedList(Node currEar, final List 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 + if (splitEarcut(currEar, tessellation, mortonOptimized) == false) { + //we could not process all points. Tessellation failed + tessellation.clear(); + } + 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 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 boolean mortonIsEar(final Node ear) { + // 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); + + // now make sure we don't have other points inside the potential ear; + + // look for points inside the triangle in both directions + Node p = ear.previousZ; + Node n = ear.nextZ; + while (p != null && Long.compareUnsigned(p.morton, minZ) >= 0 + && n != null && Long.compareUnsigned(n.morton, maxZ) <= 0) { + if (p.idx != ear.previous.idx && p.idx != ear.next.idx && + pointInEar(p.getX(), p.getY(), ear.previous.getX(), ear.previous.getY(), ear.getX(), ear.getY(), + ear.next.getX(), ear.next.getY()) && + area(p.previous.getX(), p.previous.getY(), p.getX(), p.getY(), p.next.getX(), p.next.getY()) >= 0) return false; + p = p.previousZ; + + if (n.idx != ear.previous.idx && n.idx != ear.next.idx && + pointInEar(n.getX(), n.getY(), ear.previous.getX(), ear.previous.getY(), ear.getX(), ear.getY(), + ear.next.getX(), ear.next.getY()) && + area(n.previous.getX(), n.previous.getY(), n.getX(), n.getY(), n.next.getX(), n.next.getY()) >= 0) return false; + n = n.nextZ; + } + + // first look for points inside the triangle in decreasing z-order + while (p != null && Long.compareUnsigned(p.morton, minZ) >= 0) { + if (p.idx != ear.previous.idx && p.idx != ear.next.idx + && pointInEar(p.getX(), p.getY(), ear.previous.getX(), ear.previous.getY(), ear.getX(), ear.getY(), + ear.next.getX(), ear.next.getY()) + && area(p.previous.getX(), p.previous.getY(), p.getX(), p.getY(), p.next.getX(), p.next.getY()) >= 0) { + return false; + } + p = p.previousZ; + } + // then look for points in increasing z-order + while (n != null && + Long.compareUnsigned(n.morton, maxZ) <= 0) { + if (n.idx != ear.previous.idx && n.idx != ear.next.idx + && pointInEar(n.getX(), n.getY(), ear.previous.getX(), ear.previous.getY(), ear.getX(), ear.getY(), + ear.next.getX(), ear.next.getY()) + && area(n.previous.getX(), n.previous.getY(), n.getX(), n.getY(), n.next.getX(), n.next.getY()) >= 0) { + return false; + } + n = n.nextZ; + } + return true; + } + + /** Iterate through all polygon nodes and remove small local self-intersections **/ + private static Node cureLocalIntersections(Node startNode, final List 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 + && isIntersectingPolygon(a, a.getX(), a.getY(), b.getX(), b.getY()) == 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. Return true if the polygon was splitted **/ + private static boolean splitEarcut(final Node start, final List 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 + if (mortonIndexed) { + sortByMortonWithReset(searchNode); + sortByMortonWithReset(splitNode); + } + earcutLinkedList(searchNode, tessellation, State.INIT, mortonIndexed); + earcutLinkedList(splitNode, tessellation, State.INIT, mortonIndexed); + // Finish the iterative search + return true; + } + diagonal = diagonal.next; + } + searchNode = searchNode.next; + } while (searchNode != start); + return false; + } + + /** Links two polygon vertices using a bridge. **/ + private static 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; + a2.next = an; + a2.nextZ = an; + 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 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 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 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 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(isVertexEquals(node, x0, y0) == false && isVertexEquals(node, x1, y1) == false) { + if (linesIntersect(node.getX(), node.getY(), nextNode.getX(), nextNode.getY(), x0, y0, x1, y1)) { + return true; + } + } + node = nextNode; + } while (node != start); + + return false; + } + + /** Determines whether two line segments intersect. **/ + public static 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. It reset the values on the z values**/ + private static void sortByMortonWithReset(Node start) { + Node next = start; + do { + next.previousZ = next.previous; + next.nextZ = next.next; + next = next.next; + } while (next != start); + sortByMorton(start); + } + + /** Interlinks polygon nodes in Z-Order. **/ + private static 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 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)) { + if (pSize != 0 && (qSize == 0 || q == null || Long.compareUnsigned(p.morton, q.morton) <= 0)) { + e = p; + p = p.nextZ; + --pSize; + } else { + 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); + } + + /** Eliminate colinear/duplicate points from the doubly linked list */ + private static 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 Node insertNode(final Polygon polygon, int index, int vertexIndex, final Node lastNode) { + final Node node = new Node(polygon, index, vertexIndex); + if(lastNode == null) { + node.previous = node; + node.previousZ = node; + node.next = node; + node.nextZ = node; + } else { + node.next = lastNode.next; + node.nextZ = lastNode.next; + 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 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 boolean isVertexEquals(final Node a, final Node b) { + return isVertexEquals(a, b.getX(), b.getY()); + } + + /** Determines if two point vertices are equal. **/ + private static boolean isVertexEquals(final Node a, final double x, final double y) { + return a.getX() == x && a.getY() == y; + } + + /** 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 boolean pointInPolygon(final List 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 { + // node index in the linked list + private final int idx; + // vertex index in the polygon + private final int vrtxIdx; + // 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, final int vertexIndex) { + this.idx = index; + this.vrtxIdx = vertexIndex; + this.polygon = polygon; + this.y = encodeLatitude(polygon.getPolyLat(vrtxIdx)); + this.x = encodeLongitude(polygon.getPolyLon(vrtxIdx)); + 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.vrtxIdx = other.vrtxIdx; + 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; + this.isSteiner = other.isSteiner; + } + + /** get the x value */ + public final double getX() { + return polygon.getPolyLon(vrtxIdx); + } + + /** get the y value */ + public final double getY() { + return polygon.getPolyLat(vrtxIdx); + } + + /** get the longitude value */ + public final double getLon() { + return polygon.getPolyLon(vrtxIdx); + } + + /** get the latitude value */ + public final double getLat() { + return polygon.getPolyLat(vrtxIdx); + } + + @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 static final class Triangle { + Node[] vertex; + + protected Triangle(Node a, Node b, Node c) { + this.vertex = new Node[] {a, b, c}; + } + + /** 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; + } + } +} diff --git a/server/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java b/server/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java index e2e177c8f0f..f0eba78690a 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java +++ b/server/src/main/java/org/elasticsearch/common/geo/ShapeRelation.java @@ -19,7 +19,7 @@ package org.elasticsearch.common.geo; -import org.apache.lucene.document.LatLonShape.QueryRelation; +import org.apache.lucene.document.XLatLonShape.QueryRelation; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java index 441cb8ac3e1..0e1413ee277 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java @@ -19,7 +19,7 @@ package org.elasticsearch.index.mapper; import org.apache.lucene.document.Field; -import org.apache.lucene.document.LatLonShape; +import org.apache.lucene.document.XLatLonShape; import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Rectangle; @@ -35,7 +35,7 @@ import java.util.ArrayList; import java.util.Arrays; /** - * FieldMapper for indexing {@link org.apache.lucene.document.LatLonShape}s. + * FieldMapper for indexing {@link XLatLonShape}s. *

* Currently Shapes can only be indexed and can only be queried using * {@link org.elasticsearch.index.query.GeoShapeQueryBuilder}, consequently @@ -97,7 +97,7 @@ public class GeoShapeFieldMapper extends BaseGeoShapeFieldMapper { return (GeoShapeFieldType) super.fieldType(); } - /** parsing logic for {@link LatLonShape} indexing */ + /** parsing logic for {@link XLatLonShape} indexing */ @Override public void parse(ParseContext context) throws IOException { try { @@ -122,35 +122,35 @@ public class GeoShapeFieldMapper extends BaseGeoShapeFieldMapper { private void indexShape(ParseContext context, Object luceneShape) { if (luceneShape instanceof GeoPoint) { GeoPoint pt = (GeoPoint) luceneShape; - indexFields(context, LatLonShape.createIndexableFields(name(), pt.lat(), pt.lon())); + indexFields(context, XLatLonShape.createIndexableFields(name(), pt.lat(), pt.lon())); } else if (luceneShape instanceof double[]) { double[] pt = (double[]) luceneShape; - indexFields(context, LatLonShape.createIndexableFields(name(), pt[1], pt[0])); + indexFields(context, XLatLonShape.createIndexableFields(name(), pt[1], pt[0])); } else if (luceneShape instanceof Line) { - indexFields(context, LatLonShape.createIndexableFields(name(), (Line)luceneShape)); + indexFields(context, XLatLonShape.createIndexableFields(name(), (Line)luceneShape)); } else if (luceneShape instanceof Polygon) { - indexFields(context, LatLonShape.createIndexableFields(name(), (Polygon) luceneShape)); + indexFields(context, XLatLonShape.createIndexableFields(name(), (Polygon) luceneShape)); } else if (luceneShape instanceof double[][]) { double[][] pts = (double[][])luceneShape; for (int i = 0; i < pts.length; ++i) { - indexFields(context, LatLonShape.createIndexableFields(name(), pts[i][1], pts[i][0])); + indexFields(context, XLatLonShape.createIndexableFields(name(), pts[i][1], pts[i][0])); } } else if (luceneShape instanceof Line[]) { Line[] lines = (Line[]) luceneShape; for (int i = 0; i < lines.length; ++i) { - indexFields(context, LatLonShape.createIndexableFields(name(), lines[i])); + indexFields(context, XLatLonShape.createIndexableFields(name(), lines[i])); } } else if (luceneShape instanceof Polygon[]) { Polygon[] polys = (Polygon[]) luceneShape; for (int i = 0; i < polys.length; ++i) { - indexFields(context, LatLonShape.createIndexableFields(name(), polys[i])); + indexFields(context, XLatLonShape.createIndexableFields(name(), polys[i])); } } else if (luceneShape instanceof Rectangle) { // index rectangle as a polygon Rectangle r = (Rectangle) luceneShape; Polygon p = new Polygon(new double[]{r.minLat, r.minLat, r.maxLat, r.maxLat, r.minLat}, new double[]{r.minLon, r.maxLon, r.maxLon, r.minLon, r.minLon}); - indexFields(context, LatLonShape.createIndexableFields(name(), p)); + indexFields(context, XLatLonShape.createIndexableFields(name(), p)); } else if (luceneShape instanceof Object[]) { // recurse to index geometry collection for (Object o : (Object[])luceneShape) { diff --git a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java index 6ee0f3f10dd..96bd77725bd 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java @@ -19,7 +19,7 @@ package org.elasticsearch.index.query; -import org.apache.lucene.document.LatLonShape; +import org.apache.lucene.document.XLatLonShape; import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Polygon; import org.apache.lucene.geo.Rectangle; @@ -429,16 +429,16 @@ public class GeoShapeQueryBuilder extends AbstractQueryBuilder