Browse Source

[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.
Nick Knize 6 years ago
parent
commit
20b58f0b0f

+ 12 - 0
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

+ 373 - 0
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.
+ * <p>
+ * {@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.
+ * <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 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<Triangle> tessellation = XTessellator.tessellate(polygon);
+    List<LatLonTriangle> 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;
+  }
+}

+ 96 - 0
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.
+ *
+ * <p>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();
+  }
+}

+ 134 - 0
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}.
+ * <p>
+ * Note:
+ * <ul>
+ *    <li>{@code QueryRelation.WITHIN} queries are not yet supported</li>
+ *    <li>Dateline crossing is not yet supported</li>
+ * </ul>
+ * <p>
+ * todo:
+ * <ul>
+ *   <li>Add distance support for buffered queries</li>
+ * </ul>
+ * <p>The field must be indexed using
+ * {@link 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;
+  }
+}

+ 123 - 0
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.
+ *
+ * <p>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;
+  }
+}

+ 364 - 0
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;
+    }
+  }
+}

+ 317 - 0
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();
+  }
+}

+ 889 - 0
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.
+ * <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
+ */
+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<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, 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<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 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<Node> 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<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
+              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<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
+          && 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<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
+          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<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 {
+    // 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;
+    }
+  }
+}

+ 1 - 1
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;

+ 11 - 11
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.
  * <p>
  * 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) {

+ 7 - 7
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<GeoShapeQueryBuil
     private Query getVectorQueryFromShape(QueryShardContext context, Object queryShape) {
         Query geoQuery;
         if (queryShape instanceof Line[]) {
-            geoQuery = LatLonShape.newLineQuery(fieldName(), relation.getLuceneRelation(), (Line[]) queryShape);
+            geoQuery = XLatLonShape.newLineQuery(fieldName(), relation.getLuceneRelation(), (Line[]) queryShape);
         } else if (queryShape instanceof Polygon[]) {
-            geoQuery = LatLonShape.newPolygonQuery(fieldName(), relation.getLuceneRelation(), (Polygon[]) queryShape);
+            geoQuery = XLatLonShape.newPolygonQuery(fieldName(), relation.getLuceneRelation(), (Polygon[]) queryShape);
         } else if (queryShape instanceof Line) {
-            geoQuery = LatLonShape.newLineQuery(fieldName(), relation.getLuceneRelation(), (Line) queryShape);
+            geoQuery = XLatLonShape.newLineQuery(fieldName(), relation.getLuceneRelation(), (Line) queryShape);
         } else if (queryShape instanceof Polygon) {
-            geoQuery = LatLonShape.newPolygonQuery(fieldName(), relation.getLuceneRelation(), (Polygon) queryShape);
+            geoQuery = XLatLonShape.newPolygonQuery(fieldName(), relation.getLuceneRelation(), (Polygon) queryShape);
         } else if (queryShape instanceof Rectangle) {
             Rectangle r = (Rectangle) queryShape;
-            geoQuery = LatLonShape.newBoxQuery(fieldName(), relation.getLuceneRelation(),
+            geoQuery = XLatLonShape.newBoxQuery(fieldName(), relation.getLuceneRelation(),
                 r.minLat, r.maxLat, r.minLon, r.maxLon);
         } else if (queryShape instanceof double[][]) {
             // note: we decompose point queries into a bounding box query with min values == max values
@@ -457,7 +457,7 @@ public class GeoShapeQueryBuilder extends AbstractQueryBuilder<GeoShapeQueryBuil
                         + "But found length " + pt.length + " for field [" + fieldName + "]");
                 }
             }
-            return LatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(), pt[1], pt[1], pt[0], pt[0]);
+            return XLatLonShape.newBoxQuery(fieldName, relation.getLuceneRelation(), pt[1], pt[1], pt[0], pt[0]);
         } else if (queryShape instanceof Object[]) {
             geoQuery = createGeometryCollectionQuery(context, (Object[]) queryShape);
         } else {

+ 3 - 2
x-pack/plugin/sql/sql-cli/build.gradle

@@ -74,8 +74,9 @@ artifacts  {
     nodeps nodepsJar
 }
 
-
-tasks.withType(CheckForbiddenApis) {
+forbiddenApisMain {
+    //sql does not depend on server, so only jdk signatures should be checked
+    replaceSignatureFiles 'jdk-signatures'
     signaturesFiles += files('src/forbidden/cli-signatures.txt')
 }