You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by nk...@apache.org on 2018/08/02 23:06:28 UTC

lucene-solr:branch_7x: LUCENE-8440: Add support for indexing and searching Line and Point shapes using LatLonShape encoding

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_7x 724a65a60 -> b2b9ecb4f


LUCENE-8440: Add support for indexing and searching Line and Point shapes using LatLonShape encoding


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/b2b9ecb4
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/b2b9ecb4
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/b2b9ecb4

Branch: refs/heads/branch_7x
Commit: b2b9ecb4f090c4a345fdafbe395c72ff0c922591
Parents: 724a65a
Author: Nicholas Knize <nk...@gmail.com>
Authored: Tue Jul 31 17:45:12 2018 -0500
Committer: Nicholas Knize <nk...@gmail.com>
Committed: Thu Aug 2 17:54:55 2018 -0500

----------------------------------------------------------------------
 lucene/CHANGES.txt                              |   2 +
 .../src/java/org/apache/lucene/geo/Polygon.java |   2 +-
 .../org/apache/lucene/document/LatLonShape.java |  67 ++-
 .../src/java/org/apache/lucene/geo/Line.java    | 139 ++++++
 .../document/BaseLatLonShapeTestCase.java       | 457 +++++++++++++++++++
 .../document/TestLatLonLineShapeQueries.java    |  94 ++++
 .../document/TestLatLonPointShapeQueries.java   |  66 +++
 .../document/TestLatLonPolygonShapeQueries.java | 385 ++--------------
 .../apache/lucene/document/TestLatLonShape.java |  31 +-
 9 files changed, 885 insertions(+), 358 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b2b9ecb4/lucene/CHANGES.txt
----------------------------------------------------------------------
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 745d996..f58c993 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -84,6 +84,8 @@ Changes in Runtime Behavior:
 
 Improvements
 
+* LUCENE-8440: Add support for indexing and searching Line and Point shapes using LatLonShape encoding (Nick Knize)
+
 * LUCENE-8435: Add new LatLonShapePolygonQuery for querying indexed LatLonShape fields by arbitrary polygons (Nick Knize)
 
 * LUCENE-8367: Make per-dimension drill down optional for each facet dimension (Mike McCandless)

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b2b9ecb4/lucene/core/src/java/org/apache/lucene/geo/Polygon.java
----------------------------------------------------------------------
diff --git a/lucene/core/src/java/org/apache/lucene/geo/Polygon.java b/lucene/core/src/java/org/apache/lucene/geo/Polygon.java
index 5e14286..a6d7e9d 100644
--- a/lucene/core/src/java/org/apache/lucene/geo/Polygon.java
+++ b/lucene/core/src/java/org/apache/lucene/geo/Polygon.java
@@ -202,7 +202,7 @@ public final class Polygon {
     return sb.toString();
   }
 
-  private String verticesToGeoJSON(final double[] lats, final double[] lons) {
+  public static String verticesToGeoJSON(final double[] lats, final double[] lons) {
     StringBuilder sb = new StringBuilder();
     sb.append('[');
     for (int i = 0; i < lats.length; i++) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b2b9ecb4/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java
index 28c95e4..01a31ad 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonShape.java
@@ -19,6 +19,7 @@ package org.apache.lucene.document;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.lucene.geo.Line;
 import org.apache.lucene.geo.Polygon;
 import org.apache.lucene.geo.Tessellator;
 import org.apache.lucene.geo.Tessellator.Triangle;
@@ -27,6 +28,9 @@ import org.apache.lucene.search.Query;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.NumericUtils;
 
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
+import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
+
 /**
  * An indexed shape utility class.
  * <p>
@@ -62,16 +66,67 @@ public class LatLonShape {
   private LatLonShape() {
   }
 
-  /** the lionshare of the indexing is done by the tessellator */
+  /** 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 = Tessellator.tessellate(polygon);
     List<LatLonTriangle> fields = new ArrayList<>();
-    for (int i = 0; i < tessellation.size(); ++i) {
-      fields.add(new LatLonTriangle(fieldName, tessellation.get(i)));
+    for (Triangle t : tessellation) {
+      fields.add(new LatLonTriangle(fieldName, t.getEncodedX(0), t.getEncodedY(0),
+          t.getEncodedX(1), t.getEncodedY(1), t.getEncodedX(2), t.getEncodedY(2)));
     }
     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();
+    List<LatLonTriangle> fields = new ArrayList<>(numPoints - 1);
+
+    // encode the line vertices
+    int[] encodedLats = new int[numPoints];
+    int[] encodedLons = new int[numPoints];
+    for (int i = 0; i < numPoints; ++i) {
+      encodedLats[i] = encodeLatitude(line.getLat(i));
+      encodedLons[i] = encodeLongitude(line.getLon(i));
+    }
+
+    // create "flat" triangles
+    int aLat, bLat, aLon, bLon, temp;
+    for (int i = 0, j = 1; j < numPoints; ++i, ++j) {
+      aLat = encodedLats[i];
+      aLon = encodedLons[i];
+      bLat = encodedLats[j];
+      bLon = encodedLons[j];
+      if (aLat > bLat) {
+        temp = aLat;
+        aLat = bLat;
+        bLat = temp;
+        temp = aLon;
+        aLon = bLon;
+        bLon = temp;
+      } else if (aLat == bLat) {
+        if (aLon > bLon) {
+          temp = aLat;
+          aLat = bLat;
+          bLat = temp;
+          temp = aLon;
+          aLon = bLon;
+          bLon = temp;
+        }
+      }
+      fields.add(new LatLonTriangle(fieldName, aLon, aLat, bLon, bLat, aLon, aLat));
+    }
+    return fields.toArray(new Field[fields.size()]);
+  }
+
+  /** create indexable fields for point geometry */
+  public static Field[] createIndexableFields(String fieldName, double lat, double lon) {
+    final int encodedLat = encodeLatitude(lat);
+    final int encodedLon = encodeLongitude(lon);
+    return new Field[] {new LatLonTriangle(fieldName, encodedLon, encodedLat, encodedLon, encodedLat, encodedLon, encodedLat)};
+  }
+
   /** create a query to find all polygons that intersect a defined bounding box
    *  note: does not currently support dateline crossing boxes
    * todo split dateline crossing boxes into two queries like {@link LatLonPoint#newBoxQuery}
@@ -89,11 +144,9 @@ public class LatLonShape {
    */
   private static class LatLonTriangle extends Field {
 
-    public LatLonTriangle(String name, Triangle t) {
+    LatLonTriangle(String name, int ax, int ay, int bx, int by, int cx, int cy) {
       super(name, TYPE);
-      setTriangleValue(t.getEncodedX(0), t.getEncodedY(0),
-                       t.getEncodedX(1), t.getEncodedY(1),
-                       t.getEncodedX(2), t.getEncodedY(2));
+      setTriangleValue(ax, ay, bx, by, cx, cy);
     }
 
     public void setTriangleValue(int aX, int aY, int bX, int bY, int cX, int cY) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b2b9ecb4/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java b/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java
new file mode 100644
index 0000000..c7e626d
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/geo/Line.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.geo;
+
+import java.util.Arrays;
+
+/**
+ * Represents a line on the earth's surface.  You can construct the Line directly with {@code double[]}
+ * coordinates.
+ * <p>
+ * NOTES:
+ * <ol>
+ *   <li>All latitude/longitude values must be in decimal degrees.
+ *   <li>For more advanced GeoSpatial indexing and query operations see the {@code spatial-extras} module
+ * </ol>
+ * @lucene.experimental
+ */
+public class Line {
+  /** array of latitude coordinates */
+  private final double[] lats;
+  /** array of longitude coordinates */
+  private final double[] lons;
+
+  /** minimum latitude of this line's bounding box */
+  public final double minLat;
+  /** maximum latitude of this line's bounding box */
+  public final double maxLat;
+  /** minimum longitude of this line's bounding box */
+  public final double minLon;
+  /** maximum longitude of this line's bounding box */
+  public final double maxLon;
+
+  /**
+   * Creates a new Line from the supplied latitude/longitude array.
+   */
+  public Line(double[] lats, double[] lons) {
+    if (lats == null) {
+      throw new IllegalArgumentException("lats must not be null");
+    }
+    if (lons == null) {
+      throw new IllegalArgumentException("lons must not be null");
+    }
+    if (lats.length != lons.length) {
+      throw new IllegalArgumentException("lats and lons must be equal length");
+    }
+    if (lats.length < 2) {
+      throw new IllegalArgumentException("at least 2 line points required");
+    }
+
+    // compute bounding box
+    double minLat = lats[0];
+    double minLon = lons[0];
+    double maxLat = lats[0];
+    double maxLon = lons[0];
+    for (int i = 0; i < lats.length; ++i) {
+      GeoUtils.checkLatitude(lats[i]);
+      GeoUtils.checkLongitude(lons[i]);
+      minLat = Math.min(lats[i], minLat);
+      minLon = Math.min(lons[i], minLon);
+      maxLat = Math.max(lats[i], maxLat);
+      maxLon = Math.max(lons[i], maxLon);
+    }
+
+    this.lats = lats.clone();
+    this.lons = lons.clone();
+    this.minLat = minLat;
+    this.maxLat = maxLat;
+    this.minLon = minLon;
+    this.maxLon = maxLon;
+  }
+
+  /** returns the number of vertex points */
+  public int numPoints() {
+    return lats.length;
+  }
+
+  /** Returns latitude value at given index */
+  public double getLat(int vertex) {
+    return lats[vertex];
+  }
+
+  /** Returns longitude value at given index */
+  public double getLon(int vertex) {
+    return lons[vertex];
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (!(o instanceof Line)) return false;
+    Line line = (Line) o;
+    return Arrays.equals(lats, line.lats) && Arrays.equals(lons, line.lons);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = Arrays.hashCode(lats);
+    result = 31 * result + Arrays.hashCode(lons);
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("LINE(");
+    for (int i = 0; i < lats.length; i++) {
+      sb.append("[")
+          .append(lats[i])
+          .append(", ")
+          .append(lons[i])
+          .append("]");
+    }
+    sb.append(')');
+    return sb.toString();
+  }
+
+  /** prints polygons as geojson */
+  public String toGeoJSON() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("[");
+    sb.append(Polygon.verticesToGeoJSON(lats, lons));
+    sb.append("]");
+    return sb.toString();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b2b9ecb4/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java
new file mode 100644
index 0000000..1ca505e
--- /dev/null
+++ b/lucene/sandbox/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java
@@ -0,0 +1,457 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.document;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+import org.apache.lucene.geo.GeoTestUtil;
+import org.apache.lucene.geo.Line;
+import org.apache.lucene.geo.Polygon;
+import org.apache.lucene.geo.Polygon2D;
+import org.apache.lucene.geo.Rectangle;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.MultiDocValues;
+import org.apache.lucene.index.MultiFields;
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.index.SerialMergeScheduler;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.SimpleCollector;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.Bits;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.LuceneTestCase;
+
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomBoolean;
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomInt;
+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.GeoTestUtil.nextLatitude;
+import static org.apache.lucene.geo.GeoTestUtil.nextLongitude;
+
+public abstract class BaseLatLonShapeTestCase extends LuceneTestCase {
+
+  protected static final String FIELD_NAME = "shape";
+
+  protected abstract ShapeType getShapeType();
+
+  protected Object nextShape() {
+    return getShapeType().nextShape();
+  }
+
+  protected double quantizeLat(double rawLat) {
+    return decodeLatitude(encodeLatitude(rawLat));
+  }
+
+  protected double quantizeLatCeil(double rawLat) {
+    return decodeLatitude(encodeLatitudeCeil(rawLat));
+  }
+
+  protected double quantizeLon(double rawLon) {
+    return decodeLongitude(encodeLongitude(rawLon));
+  }
+
+  protected double quantizeLonCeil(double rawLon) {
+    return decodeLongitude(encodeLongitudeCeil(rawLon));
+  }
+
+  protected Polygon quantizePolygon(Polygon polygon) {
+    double[] lats = new double[polygon.numPoints()];
+    double[] lons = new double[polygon.numPoints()];
+    for (int i = 0; i < lats.length; ++i) {
+      lats[i] = quantizeLat(polygon.getPolyLat(i));
+      lons[i] = quantizeLon(polygon.getPolyLon(i));
+    }
+    return new Polygon(lats, lons);
+  }
+
+  protected abstract Field[] createIndexableFields(String field, Object shape);
+
+  private void addShapeToDoc(String field, Document doc, Object shape) {
+    Field[] fields = createIndexableFields(field, shape);
+    for (Field f : fields) {
+      doc.add(f);
+    }
+  }
+
+  protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
+    return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon);
+  }
+
+  protected Query newPolygonQuery(String field, Polygon... polygons) {
+    return LatLonShape.newPolygonQuery(field, polygons);
+  }
+
+  public void testRandomTiny() throws Exception {
+    // Make sure single-leaf-node case is OK:
+    doTestRandom(10);
+  }
+
+  public void testRandomMedium() throws Exception {
+    doTestRandom(10000);
+  }
+
+  @Nightly
+  public void testRandomBig() throws Exception {
+    doTestRandom(50000);
+  }
+
+  private void doTestRandom(int count) throws Exception {
+    int numShapes = atLeast(count);
+    ShapeType type = getShapeType();
+
+    if (VERBOSE) {
+      System.out.println("TEST: number of " + type.name() + " shapes=" + numShapes);
+    }
+
+    Object[] shapes = new Object[numShapes];
+    for (int id = 0; id < numShapes; ++id) {
+      int x = randomInt(20);
+      if (x == 17) {
+        shapes[id] = null;
+        if (VERBOSE) {
+          System.out.println("  id=" + id + " is missing");
+        }
+      } else {
+        // create a new shape
+        shapes[id] = nextShape();
+      }
+    }
+    verify(shapes);
+  }
+
+  private void verify(Object... shapes) throws Exception {
+    IndexWriterConfig iwc = newIndexWriterConfig();
+    iwc.setMergeScheduler(new SerialMergeScheduler());
+    int mbd = iwc.getMaxBufferedDocs();
+    if (mbd != -1 && mbd < shapes.length / 100) {
+      iwc.setMaxBufferedDocs(shapes.length / 100);
+    }
+    Directory dir;
+    if (shapes.length > 1000) {
+      dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
+    } else {
+      dir = newDirectory();
+    }
+    IndexWriter w = new IndexWriter(dir, iwc);
+
+    // index random polygons
+    indexRandomShapes(w, shapes);
+
+    // query testing
+    final IndexReader reader = DirectoryReader.open(w);
+
+    // test random bbox queries
+    verifyRandomBBoxQueries(reader, shapes);
+    // test random polygon queires
+    verifyRandomPolygonQueries(reader, shapes);
+
+    IOUtils.close(w, reader, dir);
+  }
+
+  protected void indexRandomShapes(IndexWriter w, Object... shapes) throws Exception {
+    Set<Integer> deleted = new HashSet<>();
+    for (int id = 0; id < shapes.length; ++id) {
+      Document doc = new Document();
+      doc.add(newStringField("id", "" + id, Field.Store.NO));
+      doc.add(new NumericDocValuesField("id", id));
+      if (shapes[id] != null) {
+        addShapeToDoc(FIELD_NAME, doc, shapes[id]);
+      }
+      w.addDocument(doc);
+      if (id > 0 && randomInt(100) == 42) {
+        int idToDelete = randomInt(id);
+        w.deleteDocuments(new Term("id", ""+idToDelete));
+        deleted.add(idToDelete);
+        if (VERBOSE) {
+          System.out.println("   delete id=" + idToDelete);
+        }
+      }
+    }
+
+    if (randomBoolean()) {
+      w.forceMerge(1);
+    }
+  }
+
+  protected void verifyRandomBBoxQueries(IndexReader reader, Object... shapes) throws Exception {
+    IndexSearcher s = newSearcher(reader);
+
+    final int iters = atLeast(75);
+
+    Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
+    int maxDoc = s.getIndexReader().maxDoc();
+
+    for (int iter = 0; iter < iters; ++iter) {
+      if (VERBOSE) {
+        System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
+      }
+
+      // BBox
+      Rectangle rect;
+      // quantizing the bbox may end up w/ bounding boxes crossing dateline...
+      // todo add support for bounding boxes crossing dateline
+      while (true) {
+        rect = GeoTestUtil.nextBoxNotCrossingDateline();
+        if (decodeLongitude(encodeLongitudeCeil(rect.minLon)) <= decodeLongitude(encodeLongitude(rect.maxLon)) &&
+            decodeLatitude(encodeLatitudeCeil(rect.minLat)) <= decodeLatitude(encodeLatitude(rect.maxLat))) {
+          break;
+        }
+      }
+      Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
+
+      if (VERBOSE) {
+        System.out.println("  query=" + query);
+      }
+
+      final FixedBitSet hits = new FixedBitSet(maxDoc);
+      s.search(query, new SimpleCollector() {
+
+        private int docBase;
+
+        @Override
+        public boolean needsScores() {
+          return false;
+        }
+
+        @Override
+        protected void doSetNextReader(LeafReaderContext context) throws IOException {
+          docBase = context.docBase;
+        }
+
+        @Override
+        public void collect(int doc) throws IOException {
+          hits.set(docBase+doc);
+        }
+      });
+
+      boolean fail = false;
+      NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
+      for (int docID = 0; docID < maxDoc; ++docID) {
+        assertEquals(docID, docIDToID.nextDoc());
+        int id = (int) docIDToID.longValue();
+        boolean expected;
+        if (liveDocs != null && liveDocs.get(docID) == false) {
+          // document is deleted
+          expected = false;
+        } else if (shapes[id] == null) {
+          expected = false;
+        } else {
+          // check quantized poly against quantized query
+          expected = getValidator().testBBoxQuery(quantizeLatCeil(rect.minLat), quantizeLat(rect.maxLat),
+              quantizeLonCeil(rect.minLon), quantizeLon(rect.maxLon), shapes[id]);
+        }
+
+        if (hits.get(docID) != expected) {
+          StringBuilder b = new StringBuilder();
+
+          if (expected) {
+            b.append("FAIL: id=" + id + " should match but did not\n");
+          } else {
+            b.append("FAIL: id=" + id + " should not match but did\n");
+          }
+          b.append("  query=" + query + " docID=" + docID + "\n");
+          b.append("  shape=" + shapes[id] + "\n");
+          b.append("  deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
+          b.append("  rect=Rectangle(" + quantizeLatCeil(rect.minLat) + " TO " + quantizeLat(rect.maxLat) + " lon=" + quantizeLonCeil(rect.minLon) + " TO " + quantizeLon(rect.maxLon) + ")");
+          if (true) {
+            fail("wrong hit (first of possibly more):\n\n" + b);
+          } else {
+            System.out.println(b.toString());
+            fail = true;
+          }
+        }
+      }
+      if (fail) {
+        fail("some hits were wrong");
+      }
+    }
+  }
+
+  protected void verifyRandomPolygonQueries(IndexReader reader, Object... shapes) throws Exception {
+    IndexSearcher s = newSearcher(reader);
+
+    final int iters = atLeast(75);
+
+    Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
+    int maxDoc = s.getIndexReader().maxDoc();
+
+    for (int iter = 0; iter < iters; ++iter) {
+      if (VERBOSE) {
+        System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s);
+      }
+
+      // Polygon
+      Polygon queryPolygon = GeoTestUtil.nextPolygon();
+      Polygon2D queryPoly2D = Polygon2D.create(queryPolygon);
+      Query query = newPolygonQuery(FIELD_NAME, queryPolygon);
+
+      if (VERBOSE) {
+        System.out.println("  query=" + query);
+      }
+
+      final FixedBitSet hits = new FixedBitSet(maxDoc);
+      s.search(query, new SimpleCollector() {
+
+        private int docBase;
+
+        @Override
+        public boolean needsScores() {
+          return false;
+        }
+
+        @Override
+        protected void doSetNextReader(LeafReaderContext context) throws IOException {
+          docBase = context.docBase;
+        }
+
+        @Override
+        public void collect(int doc) throws IOException {
+          hits.set(docBase+doc);
+        }
+      });
+
+      boolean fail = false;
+      NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
+      for (int docID = 0; docID < maxDoc; ++docID) {
+        assertEquals(docID, docIDToID.nextDoc());
+        int id = (int) docIDToID.longValue();
+        boolean expected;
+        if (liveDocs != null && liveDocs.get(docID) == false) {
+          // document is deleted
+          expected = false;
+        } else if (shapes[id] == null) {
+          expected = false;
+        } else {
+          expected = getValidator().testPolygonQuery(queryPoly2D, shapes[id]);
+        }
+
+        if (hits.get(docID) != expected) {
+          StringBuilder b = new StringBuilder();
+
+          if (expected) {
+            b.append("FAIL: id=" + id + " should match but did not\n");
+          } else {
+            b.append("FAIL: id=" + id + " should not match but did\n");
+          }
+          b.append("  query=" + query + " docID=" + docID + "\n");
+          b.append("  shape=" + shapes[id] + "\n");
+          b.append("  deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
+          b.append("  queryPolygon=" + queryPolygon.toGeoJSON());
+          if (true) {
+            fail("wrong hit (first of possibly more):\n\n" + b);
+          } else {
+            System.out.println(b.toString());
+            fail = true;
+          }
+        }
+      }
+      if (fail) {
+        fail("some hits were wrong");
+      }
+    }
+  }
+
+  protected abstract Validator getValidator();
+
+  /** internal point class for testing point shapes */
+  protected static class Point {
+    double lat;
+    double lon;
+
+    public Point(double lat, double lon) {
+      this.lat = lat;
+      this.lon = lon;
+    }
+
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("POINT(");
+      sb.append(lon);
+      sb.append(',');
+      sb.append(lat);
+      return sb.toString();
+    }
+  }
+
+  /** internal shape type for testing different shape types */
+  protected enum ShapeType {
+    POINT() {
+      public Point nextShape() {
+        return new Point(nextLatitude(), nextLongitude());
+      }
+    },
+    LINE() {
+      public Line nextShape() {
+        Polygon p = GeoTestUtil.nextPolygon();
+        double[] lats = new double[p.numPoints() - 1];
+        double[] lons = new double[lats.length];
+        for (int i = 0; i < lats.length; ++i) {
+          lats[i] = p.getPolyLat(i);
+          lons[i] = p.getPolyLon(i);
+        }
+        return new Line(lats, lons);
+      }
+    },
+    POLYGON() {
+      public Polygon nextShape() {
+        return GeoTestUtil.nextPolygon();
+      }
+    },
+    MIXED() {
+      public Object nextShape() {
+        return RandomPicks.randomFrom(random(), subList).nextShape();
+      }
+    };
+
+    static ShapeType[] subList;
+    static {
+      subList = new ShapeType[] {POINT, LINE, POLYGON};
+    }
+
+    public abstract Object nextShape();
+
+    static ShapeType fromObject(Object shape) {
+      if (shape instanceof Point) {
+        return POINT;
+      } else if (shape instanceof Line) {
+        return LINE;
+      } else if (shape instanceof Polygon) {
+        return POLYGON;
+      }
+      throw new IllegalArgumentException("invalid shape type from " + shape.toString());
+    }
+  }
+
+  protected interface Validator {
+    boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape);
+    boolean testPolygonQuery(Polygon2D poly2d, Object shape);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b2b9ecb4/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java
new file mode 100644
index 0000000..21367dc
--- /dev/null
+++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonLineShapeQueries.java
@@ -0,0 +1,94 @@
+/*
+ * 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.Line;
+import org.apache.lucene.geo.Polygon;
+import org.apache.lucene.geo.Polygon2D;
+import org.apache.lucene.index.PointValues.Relation;
+
+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.encodeLongitude;
+
+/** random bounding box and polygon query tests for random generated {@link Line} types */
+public class TestLatLonLineShapeQueries extends BaseLatLonShapeTestCase {
+
+  protected final LineValidator VALIDATOR = new LineValidator();
+
+  @Override
+  protected ShapeType getShapeType() {
+    return ShapeType.LINE;
+  }
+
+  @Override
+  protected Field[] createIndexableFields(String field, Object line) {
+    return LatLonShape.createIndexableFields(field, (Line)line);
+  }
+
+  @Override
+  protected Validator getValidator() {
+    return VALIDATOR;
+  }
+
+  protected class LineValidator implements Validator {
+    @Override
+    public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
+      // to keep it simple we convert the bbox into a polygon and use poly2d
+      Polygon2D p = Polygon2D.create(new Polygon[] {new Polygon(new double[] {minLat, minLat, maxLat, maxLat, minLat},
+          new double[] {minLon, maxLon, maxLon, minLon, minLon})});
+      return testLine(p, (Line)shape);
+    }
+
+    @Override
+    public boolean testPolygonQuery(Polygon2D poly2d, Object shape) {
+      return testLine(poly2d, (Line) shape);
+    }
+
+    private boolean testLine(Polygon2D queryPoly, Line line) {
+      double ax, ay, bx, by, temp;
+      for (int i = 0, j = 1; j < line.numPoints(); ++i, ++j) {
+        ay = decodeLatitude(encodeLatitude(line.getLat(i)));
+        ax = decodeLongitude(encodeLongitude(line.getLon(i)));
+        by = decodeLatitude(encodeLatitude(line.getLat(j)));
+        bx = decodeLongitude(encodeLongitude(line.getLon(j)));
+        if (ay > by) {
+          temp = ay;
+          ay = by;
+          by = temp;
+          temp = ax;
+          ax = bx;
+          bx = temp;
+        } else if (ay == by) {
+          if (ax > bx) {
+            temp = ay;
+            ay = by;
+            by = temp;
+            temp = ax;
+            ax = bx;
+            bx = temp;
+          }
+        }
+        if (queryPoly.relateTriangle(ax, ay, bx, by, ax, ay) != Relation.CELL_OUTSIDE_QUERY) {
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b2b9ecb4/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java
new file mode 100644
index 0000000..e98cb73
--- /dev/null
+++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPointShapeQueries.java
@@ -0,0 +1,66 @@
+/*
+ * 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.Polygon2D;
+import org.apache.lucene.index.PointValues.Relation;
+
+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.encodeLongitude;
+
+/** random bounding box and polygon query tests for random generated {@code latitude, longitude} points */
+public class TestLatLonPointShapeQueries extends BaseLatLonShapeTestCase {
+
+  protected final PointValidator VALIDATOR = new PointValidator();
+
+  @Override
+  protected ShapeType getShapeType() {
+    return ShapeType.POINT;
+  }
+
+  @Override
+  protected Field[] createIndexableFields(String field, Object point) {
+    Point p = (Point)point;
+    return LatLonShape.createIndexableFields(field, p.lat, p.lon);
+  }
+
+  @Override
+  protected Validator getValidator() {
+    return VALIDATOR;
+  }
+
+  protected class PointValidator implements Validator {
+    @Override
+    public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
+      Point p = (Point)shape;
+      double lat = decodeLatitude(encodeLatitude(p.lat));
+      double lon = decodeLongitude(encodeLongitude(p.lon));
+      return (lat < minLat || lat > maxLat || lon < minLon || lon > maxLon) == false;
+    }
+
+    @Override
+    public boolean testPolygonQuery(Polygon2D poly2d, Object shape) {
+      Point p = (Point) shape;
+      double lat = decodeLatitude(encodeLatitude(p.lat));
+      double lon = decodeLongitude(encodeLongitude(p.lon));
+      // for consistency w/ the query we test the point as a triangle
+      return poly2d.relateTriangle(lon, lat, lon, lat, lon, lat) != Relation.CELL_OUTSIDE_QUERY;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b2b9ecb4/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java
index e6bd907..de9fd4f 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonPolygonShapeQueries.java
@@ -16,377 +16,68 @@
  */
 package org.apache.lucene.document;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
-import org.apache.lucene.geo.GeoTestUtil;
 import org.apache.lucene.geo.Polygon;
 import org.apache.lucene.geo.Polygon2D;
-import org.apache.lucene.geo.Rectangle;
 import org.apache.lucene.geo.Tessellator;
-import org.apache.lucene.index.DirectoryReader;
-import org.apache.lucene.index.IndexReader;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.IndexWriterConfig;
-import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.MultiDocValues;
-import org.apache.lucene.index.MultiFields;
-import org.apache.lucene.index.NumericDocValues;
 import org.apache.lucene.index.PointValues.Relation;
-import org.apache.lucene.index.SerialMergeScheduler;
-import org.apache.lucene.index.Term;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.SimpleCollector;
-import org.apache.lucene.store.Directory;
-import org.apache.lucene.util.Bits;
-import org.apache.lucene.util.FixedBitSet;
-import org.apache.lucene.util.IOUtils;
-import org.apache.lucene.util.LuceneTestCase;
 
-import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitudeCeil;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
-import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitudeCeil;
+/** random bounding box and polygon query tests for random generated {@link Polygon} types */
+public class TestLatLonPolygonShapeQueries extends BaseLatLonShapeTestCase {
 
-/** base Test case for {@link LatLonShape} indexing and search */
-public class TestLatLonPolygonShapeQueries extends LuceneTestCase {
-  protected static final String FIELD_NAME = "shape";
+  protected final PolygonValidator VALIDATOR = new PolygonValidator();
 
-  private Polygon quantizePolygon(Polygon polygon) {
-    double[] lats = new double[polygon.numPoints()];
-    double[] lons = new double[polygon.numPoints()];
-    for (int i = 0; i < lats.length; ++i) {
-      lats[i] = quantizeLat(polygon.getPolyLat(i));
-      lons[i] = quantizeLon(polygon.getPolyLon(i));
-    }
-    return new Polygon(lats, lons);
-  }
-
-  protected double quantizeLat(double rawLat) {
-    return decodeLatitude(encodeLatitude(rawLat));
-  }
-
-  protected double quantizeLatCeil(double rawLat) {
-    return decodeLatitude(encodeLatitudeCeil(rawLat));
-  }
-
-  protected double quantizeLon(double rawLon) {
-    return decodeLongitude(encodeLongitude(rawLon));
-  }
-
-  protected double quantizeLonCeil(double rawLon) {
-    return decodeLongitude(encodeLongitudeCeil(rawLon));
-  }
-
-  protected void addPolygonsToDoc(String field, Document doc, Polygon polygon) {
-    Field[] fields = LatLonShape.createIndexableFields(field, polygon);
-    for (Field f : fields) {
-      doc.add(f);
-    }
-  }
-
-  protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
-    return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon);
-  }
-
-  protected Query newPolygonQuery(String field, Polygon... polygons) {
-    return LatLonShape.newPolygonQuery(field, polygons);
-  }
-
-  public void testRandomTiny() throws Exception {
-    // Make sure single-leaf-node case is OK:
-    doTestRandom(10);
-  }
-
-  public void testRandomMedium() throws Exception {
-    doTestRandom(10000);
+  @Override
+  protected ShapeType getShapeType() {
+    return ShapeType.POLYGON;
   }
 
-  @Nightly
-  public void testRandomBig() throws Exception {
-    doTestRandom(50000);
-  }
-
-  private void doTestRandom(int count) throws Exception {
-    int numPolygons = atLeast(count);
-
-    if (VERBOSE) {
-      System.out.println("TEST: numPolygons=" + numPolygons);
-    }
-
-    Polygon[] polygons = new Polygon[numPolygons];
-    for (int id = 0; id < numPolygons; ++id) {
-      int x = random().nextInt(20);
-      if (x == 17) {
-        polygons[id] = null;
-        if (VERBOSE) {
-          System.out.println("  id=" + id + " is missing");
-        }
-      } else {
-        // create a polygon that does not cross the dateline
-        polygons[id] = GeoTestUtil.nextPolygon();
+  @Override
+  protected Polygon nextShape() {
+    Polygon p;
+    while (true) {
+      // if we can't tessellate; then random polygon generator created a malformed shape
+      p = (Polygon)getShapeType().nextShape();
+      try {
+        Tessellator.tessellate(p);
+        return p;
+      } catch (IllegalArgumentException e) {
+        continue;
       }
     }
-    verify(polygons);
   }
 
-  private void verify(Polygon... polygons) throws Exception {
-    ArrayList<Polygon2D> poly2d = new ArrayList<>();
-    poly2d.ensureCapacity(polygons.length);
-    // index random polygons; poly2d will contain the Polygon2D objects needed for verification
-    IndexWriter w = indexRandomPolygons(poly2d, polygons);
-    Directory dir = w.getDirectory();
-    final IndexReader reader = DirectoryReader.open(w);
-    // test random bbox queries
-    verifyRandomBBoxQueries(reader, poly2d, polygons);
-    // test random polygon queires
-    verifyRandomPolygonQueries(reader, poly2d, polygons);
-    IOUtils.close(w, reader, dir);
+  @Override
+  protected Field[] createIndexableFields(String field, Object polygon) {
+    return LatLonShape.createIndexableFields(field, (Polygon)polygon);
   }
 
-  protected IndexWriter indexRandomPolygons(List<Polygon2D> poly2d, Polygon... polygons) throws Exception {
-    IndexWriterConfig iwc = newIndexWriterConfig();
-    iwc.setMergeScheduler(new SerialMergeScheduler());
-    int mbd = iwc.getMaxBufferedDocs();
-    if (mbd != -1 && mbd < polygons.length / 100) {
-      iwc.setMaxBufferedDocs(polygons.length / 100);
-    }
-    Directory dir;
-    if (polygons.length > 1000) {
-      dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
-    } else {
-      dir = newDirectory();
-    }
-
-    Set<Integer> deleted = new HashSet<>();
-    IndexWriter w = new IndexWriter(dir, iwc);
-    for (int id = 0; id < polygons.length; ++id) {
-      Document doc = new Document();
-      doc.add(newStringField("id", "" + id, Field.Store.NO));
-      doc.add(new NumericDocValuesField("id", id));
-      if (polygons[id] != null) {
-        try {
-          addPolygonsToDoc(FIELD_NAME, doc, polygons[id]);
-        } catch (IllegalArgumentException e) {
-          // GeoTestUtil will occassionally create invalid polygons
-          // invalid polygons will not tessellate
-          // we skip those polygons that will not tessellate, relying on the TestTessellator class
-          // to ensure the Tessellator correctly identified a malformed shape and its not a bug
-          if (VERBOSE) {
-            System.out.println("  id=" + id + " could not tessellate. Malformed shape " + polygons[id] + " detected");
-          }
-          // remove and skip the malformed shape
-          polygons[id] = null;
-          poly2d.add(id, null);
-          continue;
-        }
-        poly2d.add(id, Polygon2D.create(quantizePolygon(polygons[id])));
-      } else {
-        poly2d.add(id, null);
-      }
-      w.addDocument(doc);
-      if (id > 0 && random().nextInt(100) == 42) {
-        int idToDelete = random().nextInt(id);
-        w.deleteDocuments(new Term("id", ""+idToDelete));
-        deleted.add(idToDelete);
-        if (VERBOSE) {
-          System.out.println("   delete id=" + idToDelete);
-        }
-      }
-    }
-
-    if (random().nextBoolean()) {
-      w.forceMerge(1);
-    }
-
-    return w;
+  @Override
+  protected Validator getValidator() {
+    return VALIDATOR;
   }
 
-  protected void verifyRandomBBoxQueries(IndexReader reader, List<Polygon2D> poly2d, Polygon... polygons) throws Exception {
-    IndexSearcher s = newSearcher(reader);
-
-    final int iters = atLeast(75);
-
-    Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
-    int maxDoc = s.getIndexReader().maxDoc();
-
-    for (int iter = 0; iter < iters; ++iter) {
-      if (VERBOSE) {
-        System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
-      }
-
-      // BBox
-      Rectangle rect = GeoTestUtil.nextBoxNotCrossingDateline();
-      Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
-
-      if (VERBOSE) {
-        System.out.println("  query=" + query);
-      }
-
-      final FixedBitSet hits = new FixedBitSet(maxDoc);
-      s.search(query, new SimpleCollector() {
-
-        private int docBase;
-
-        @Override
-        public boolean needsScores() {
-          return false;
-        }
-
-        @Override
-        protected void doSetNextReader(LeafReaderContext context) throws IOException {
-          docBase = context.docBase;
-        }
-
-        @Override
-        public void collect(int doc) throws IOException {
-          hits.set(docBase+doc);
-        }
-      });
-
-      boolean fail = false;
-      NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
-      for (int docID = 0; docID < maxDoc; ++docID) {
-        assertEquals(docID, docIDToID.nextDoc());
-        int id = (int) docIDToID.longValue();
-        boolean expected;
-        if (liveDocs != null && liveDocs.get(docID) == false) {
-          // document is deleted
-          expected = false;
-        } else if (polygons[id] == null) {
-          expected = false;
-        } else {
-          // check quantized poly against quantized query
-          expected = poly2d.get(id).relate(quantizeLatCeil(rect.minLat), quantizeLat(rect.maxLat),
-              quantizeLonCeil(rect.minLon), quantizeLon(rect.maxLon)) != Relation.CELL_OUTSIDE_QUERY;
-        }
-
-        if (hits.get(docID) != expected) {
-          StringBuilder b = new StringBuilder();
-
-          if (expected) {
-            b.append("FAIL: id=" + id + " should match but did not\n");
-          } else {
-            b.append("FAIL: id=" + id + " should not match but did\n");
-          }
-          b.append("  query=" + query + " docID=" + docID + "\n");
-          b.append("  polygon=" + quantizePolygon(polygons[id]) + "\n");
-          b.append("  deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
-          b.append("  rect=Rectangle(" + quantizeLatCeil(rect.minLat) + " TO " + quantizeLat(rect.maxLat) + " lon=" + quantizeLonCeil(rect.minLon) + " TO " + quantizeLon(rect.maxLon) + ")");
-          if (true) {
-            fail("wrong hit (first of possibly more):\n\n" + b);
-          } else {
-            System.out.println(b.toString());
-            fail = true;
-          }
-        }
-      }
-      if (fail) {
-        fail("some hits were wrong");
-      }
+  protected class PolygonValidator implements Validator {
+    @Override
+    public boolean testBBoxQuery(double minLat, double maxLat, double minLon, double maxLon, Object shape) {
+      Polygon2D poly = Polygon2D.create(quantizePolygon((Polygon)shape));
+      return poly.relate(minLat, maxLat, minLon, maxLon) != Relation.CELL_OUTSIDE_QUERY;
     }
-  }
 
-  protected void verifyRandomPolygonQueries(IndexReader reader, List<Polygon2D> poly2d, Polygon... polygons) throws Exception {
-    IndexSearcher s = newSearcher(reader);
+    @Override
+    public boolean testPolygonQuery(Polygon2D query, Object shape) {
 
-    final int iters = atLeast(75);
-
-    Bits liveDocs = MultiFields.getLiveDocs(s.getIndexReader());
-    int maxDoc = s.getIndexReader().maxDoc();
-
-    for (int iter = 0; iter < iters; ++iter) {
-      if (VERBOSE) {
-        System.out.println("\nTEST: iter=" + (iter+1) + " of " + iters + " s=" + s);
-      }
-
-      // Polygon
-      Polygon queryPolygon = GeoTestUtil.nextPolygon();
-      Polygon2D queryPoly2D = Polygon2D.create(queryPolygon);
-      Query query = newPolygonQuery(FIELD_NAME, queryPolygon);
-
-      if (VERBOSE) {
-        System.out.println("  query=" + query);
-      }
-
-      final FixedBitSet hits = new FixedBitSet(maxDoc);
-      s.search(query, new SimpleCollector() {
-
-        private int docBase;
-
-        @Override
-        public boolean needsScores() {
-          return false;
+      List<Tessellator.Triangle> tessellation = Tessellator.tessellate((Polygon) shape);
+      for (Tessellator.Triangle t : tessellation) {
+        // we quantize the triangle for consistency with the index
+        if (query.relateTriangle(quantizeLon(t.getLon(0)), quantizeLat(t.getLat(0)),
+            quantizeLon(t.getLon(1)), quantizeLat(t.getLat(1)),
+            quantizeLon(t.getLon(2)), quantizeLat(t.getLat(2))) != Relation.CELL_OUTSIDE_QUERY) {
+          return true;
         }
-
-        @Override
-        protected void doSetNextReader(LeafReaderContext context) throws IOException {
-          docBase = context.docBase;
-        }
-
-        @Override
-        public void collect(int doc) throws IOException {
-          hits.set(docBase+doc);
-        }
-      });
-
-      boolean fail = false;
-      NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
-      for (int docID = 0; docID < maxDoc; ++docID) {
-        assertEquals(docID, docIDToID.nextDoc());
-        int id = (int) docIDToID.longValue();
-        boolean expected;
-        if (liveDocs != null && liveDocs.get(docID) == false) {
-          // document is deleted
-          expected = false;
-        } else if (polygons[id] == null) {
-          expected = false;
-        } else {
-          expected = false;
-          try {
-            // check poly (quantized the same way as indexed) against query polygon
-            List<Tessellator.Triangle> tesselation = Tessellator.tessellate(quantizePolygon(polygons[id]));
-            for (Tessellator.Triangle t : tesselation) {
-              if (queryPoly2D.relateTriangle(t.getLon(0), t.getLat(0),
-                  t.getLon(1), t.getLat(1), t.getLon(2), t.getLat(2)) != Relation.CELL_OUTSIDE_QUERY) {
-                expected = true;
-                break;
-              }
-            }
-          } catch (IllegalArgumentException e) {
-            continue;
-          }
-        }
-
-        if (hits.get(docID) != expected) {
-          StringBuilder b = new StringBuilder();
-
-          if (expected) {
-            b.append("FAIL: id=" + id + " should match but did not\n");
-          } else {
-            b.append("FAIL: id=" + id + " should not match but did\n");
-          }
-          b.append("  query=" + query + " docID=" + docID + "\n");
-          b.append("  polygon=" + quantizePolygon(polygons[id]).toGeoJSON() + "\n");
-          b.append("  deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
-          b.append("  queryPolygon=" + queryPolygon.toGeoJSON());
-          if (true) {
-            fail("wrong hit (first of possibly more):\n\n" + b);
-          } else {
-            System.out.println(b.toString());
-            fail = true;
-          }
-        }
-      }
-      if (fail) {
-        fail("some hits were wrong");
       }
+      return false;
     }
   }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b2b9ecb4/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShape.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShape.java b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShape.java
index f673d0a..3aa5ace 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShape.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/document/TestLatLonShape.java
@@ -18,6 +18,7 @@ package org.apache.lucene.document;
 
 import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
 import org.apache.lucene.geo.GeoTestUtil;
+import org.apache.lucene.geo.Line;
 import org.apache.lucene.geo.Polygon;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.IndexReader;
@@ -43,6 +44,13 @@ public class TestLatLonShape extends LuceneTestCase {
     }
   }
 
+  protected void addLineToDoc(String field, Document doc, Line line) {
+    Field[] fields = LatLonShape.createIndexableFields(field, line);
+    for (Field f : fields) {
+      doc.add(f);
+    }
+  }
+
   protected Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon) {
     return LatLonShape.newBoxQuery(field, minLat, maxLat, minLon, maxLon);
   }
@@ -81,19 +89,36 @@ public class TestLatLonShape extends LuceneTestCase {
     Directory dir = newDirectory();
     RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
 
-    // add a random polygon
+    // add a random polygon document
     Polygon p = GeoTestUtil.createRegularPolygon(0, 90, atLeast(1000000), numVertices);
     Document document = new Document();
     addPolygonsToDoc(FIELDNAME, document, p);
     writer.addDocument(document);
 
+    // add a line document
+    document = new Document();
+    // add a line string
+    double lats[] = new double[p.numPoints() - 1];
+    double lons[] = new double[p.numPoints() - 1];
+    for (int i = 0; i < lats.length; ++i) {
+      lats[i] = p.getPolyLat(i);
+      lons[i] = p.getPolyLon(i);
+    }
+    Line l = new Line(lats, lons);
+    addLineToDoc(FIELDNAME, document, l);
+    writer.addDocument(document);
+
     ////// search /////
     // search an intersecting bbox
     IndexReader reader = writer.getReader();
     writer.close();
     IndexSearcher searcher = newSearcher(reader);
-    Query q = newRectQuery(FIELDNAME, -1d, 1d, p.minLon, p.maxLon);
-    assertEquals(1, searcher.count(q));
+    double minLat = Math.min(lats[0], lats[1]);
+    double minLon = Math.min(lons[0], lons[1]);
+    double maxLat = Math.max(lats[0], lats[1]);
+    double maxLon = Math.max(lons[0], lons[1]);
+    Query q = newRectQuery(FIELDNAME, minLat, maxLat, minLon, maxLon);
+    assertEquals(2, searcher.count(q));
 
     // search a disjoint bbox
     q = newRectQuery(FIELDNAME, p.minLat-1d, p.minLat+1, p.minLon-1d, p.minLon+1d);