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 2016/02/05 18:03:17 UTC

[73/87] [abbrv] lucene-solr git commit: LUCENE-6997: refactors lucene-spatial module to a new lucene-spatial-extras module, and refactors sandbox GeoPointField and queries to lucene-spatial module

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test-files/simple-Queries-BBox.txt
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test-files/simple-Queries-BBox.txt b/lucene/spatial-extras/src/test-files/simple-Queries-BBox.txt
new file mode 100644
index 0000000..9f3d5b0
--- /dev/null
+++ b/lucene/spatial-extras/src/test-files/simple-Queries-BBox.txt
@@ -0,0 +1,9 @@
+C5 @ IsWithin(ENVELOPE(-6, 6, 6, -6))
+C5 @ BBoxWithin(ENVELOPE(-6, 6, 6, -6))
+C10 @ Contains(ENVELOPE(-6, 6, 6, -6))
+C10 @ IsEqualTo(ENVELOPE(-10, 10, 10, -10))
+C5 C10 @ Intersects(ENVELOPE(-2, 2, 2, -2))
+ @ Overlaps(ENVELOPE(-2, 2, 2, -2))
+C5 @ Overlaps(ENVELOPE(-2, 2, 8, -2))
+C5 C10 @ BBoxIntersects(ENVELOPE(-2, 2, 2, -2))
+NW15 @ IsDisjointTo(ENVELOPE(-10, 10, 10, -10))
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test-files/states-Intersects-BBox.txt
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test-files/states-Intersects-BBox.txt b/lucene/spatial-extras/src/test-files/states-Intersects-BBox.txt
new file mode 100644
index 0000000..a45df7b
--- /dev/null
+++ b/lucene/spatial-extras/src/test-files/states-Intersects-BBox.txt
@@ -0,0 +1,3 @@
+WY CO	@ Intersects(ENVELOPE(-106.964844, -105.734375, 42.800781, 39.460938))
+TX @ Intersects(ENVELOPE(-99.669922, -98.439453, 32.253906, 30.583984))
+MS TX LA @ Intersects(ENVELOPE(-95.363281, -90.133789, 32.473633, 29.792969))
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test-files/states-IsWithin-BBox.txt
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test-files/states-IsWithin-BBox.txt b/lucene/spatial-extras/src/test-files/states-IsWithin-BBox.txt
new file mode 100644
index 0000000..6a504da
--- /dev/null
+++ b/lucene/spatial-extras/src/test-files/states-IsWithin-BBox.txt
@@ -0,0 +1,4 @@
+KS	@ IsWithin(ENVELOPE(-103.493164, -93.825195, 41.086914, 36.208984))
+WA @ IsWithin(ENVELOPE(-126.916016, -115.314453, 50.688965, 44.36084))
+MA CT RI @ IsWithin(ENVELOPE(-73.894043, -69.521484, 43.198242, 40.825195))
+AL GA @ IsWithin(ENVELOPE(-89.472656, -80.244141, 35.90332, 29.311523))
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/DistanceStrategyTest.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/DistanceStrategyTest.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/DistanceStrategyTest.java
new file mode 100644
index 0000000..9a29677
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/DistanceStrategyTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.spatial;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.spatial.bbox.BBoxStrategy;
+import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
+import org.apache.lucene.spatial.prefix.TermQueryPrefixTreeStrategy;
+import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.PackedQuadPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
+import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
+import org.apache.lucene.spatial.vector.PointVectorStrategy;
+import org.junit.Test;
+
+public class DistanceStrategyTest extends StrategyTestCase {
+  @ParametersFactory(argumentFormatting = "strategy=%s")
+  public static Iterable<Object[]> parameters() {
+    List<Object[]> ctorArgs = new ArrayList<>();
+
+    SpatialContext ctx = SpatialContext.GEO;
+    SpatialPrefixTree grid;
+    SpatialStrategy strategy;
+
+    grid = new QuadPrefixTree(ctx,25);
+    strategy = new RecursivePrefixTreeStrategy(grid, "recursive_quad");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    grid = new GeohashPrefixTree(ctx,12);
+    strategy = new TermQueryPrefixTreeStrategy(grid, "termquery_geohash");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    grid = new PackedQuadPrefixTree(ctx,25);
+    strategy = new RecursivePrefixTreeStrategy(grid, "recursive_packedquad");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    strategy = new PointVectorStrategy(ctx, "pointvector");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    strategy = new BBoxStrategy(ctx, "bbox");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    strategy = new SerializedDVStrategy(ctx, "serialized");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    return ctorArgs;
+  }
+
+  public DistanceStrategyTest(String suiteName, SpatialStrategy strategy) {
+    this.ctx = strategy.getSpatialContext();
+    this.strategy = strategy;
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    if (strategy instanceof BBoxStrategy && random().nextBoolean()) {//disable indexing sometimes
+      BBoxStrategy bboxStrategy = (BBoxStrategy)strategy;
+      final FieldType fieldType = new FieldType(bboxStrategy.getFieldType());
+      fieldType.setIndexOptions(IndexOptions.NONE);
+      bboxStrategy.setFieldType(fieldType);
+    }
+  }
+
+  @Override
+  protected boolean needsDocValues() {
+    return true;
+  }
+
+  @Test
+  public void testDistanceOrder() throws IOException {
+    adoc("100", ctx.makePoint(2, 1));
+    adoc("101", ctx.makePoint(-1, 4));
+    adoc("103", (Shape)null);//test score for nothing
+    adoc("999", ctx.makePoint(2, 1));//test deleted
+    commit();
+    deleteDoc("999");
+    commit();
+    //FYI distances are in docid order
+    checkDistValueSource(ctx.makePoint(4, 3), 2.8274937f, 5.0898066f, 180f);
+    checkDistValueSource(ctx.makePoint(0, 4), 3.6043684f, 0.9975641f, 180f);
+  }
+
+  @Test
+  public void testRecipScore() throws IOException {
+    Point p100 = ctx.makePoint(2, 1);
+    adoc("100", p100);
+    Point p101 = ctx.makePoint(-1, 4);
+    adoc("101", p101);
+    adoc("103", (Shape)null);//test score for nothing
+    adoc("999", ctx.makePoint(2, 1));//test deleted
+    commit();
+    deleteDoc("999");
+    commit();
+
+    double dist = ctx.getDistCalc().distance(p100, p101);
+    Shape queryShape = ctx.makeCircle(2.01, 0.99, dist);
+    checkValueSource(strategy.makeRecipDistanceValueSource(queryShape),
+        new float[]{1.00f, 0.10f, 0f}, 0.09f);
+  }
+
+  void checkDistValueSource(Point pt, float... distances) throws IOException {
+    float multiplier = random().nextFloat() * 100f;
+    float[] dists2 = Arrays.copyOf(distances, distances.length);
+    for (int i = 0; i < dists2.length; i++) {
+      dists2[i] *= multiplier;
+    }
+    checkValueSource(strategy.makeDistanceValueSource(pt, multiplier), dists2, 1.0e-3f);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/PortedSolr3Test.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/PortedSolr3Test.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/PortedSolr3Test.java
new file mode 100644
index 0000000..8506c86
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/PortedSolr3Test.java
@@ -0,0 +1,167 @@
+/*
+ * 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.spatial;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.distance.DistanceUtils;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
+import org.apache.lucene.spatial.prefix.TermQueryPrefixTreeStrategy;
+import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
+import org.apache.lucene.spatial.query.SpatialArgs;
+import org.apache.lucene.spatial.query.SpatialOperation;
+import org.apache.lucene.spatial.vector.PointVectorStrategy;
+import org.junit.Test;
+
+/**
+ * Based off of Solr 3's SpatialFilterTest.
+ */
+public class PortedSolr3Test extends StrategyTestCase {
+
+  @ParametersFactory(argumentFormatting = "strategy=%s")
+  public static Iterable<Object[]> parameters() {
+    List<Object[]> ctorArgs = new ArrayList<>();
+
+    SpatialContext ctx = SpatialContext.GEO;
+    SpatialPrefixTree grid;
+    SpatialStrategy strategy;
+
+    grid = new GeohashPrefixTree(ctx,12);
+    strategy = new RecursivePrefixTreeStrategy(grid, "recursive_geohash");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    grid = new QuadPrefixTree(ctx,25);
+    strategy = new RecursivePrefixTreeStrategy(grid, "recursive_quad");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    grid = new GeohashPrefixTree(ctx,12);
+    strategy = new TermQueryPrefixTreeStrategy(grid, "termquery_geohash");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    strategy = new PointVectorStrategy(ctx, "pointvector");
+    ctorArgs.add(new Object[]{strategy.getFieldName(), strategy});
+
+    return ctorArgs;
+  }
+
+  public PortedSolr3Test(String suiteName, SpatialStrategy strategy) {
+    this.ctx = strategy.getSpatialContext();
+    this.strategy = strategy;
+  }
+
+  private void setupDocs() throws Exception {
+    super.deleteAll();
+    adoc("1", ctx.makePoint(-79.9289094, 32.7693246));
+    adoc("2", ctx.makePoint(-80.9289094, 33.7693246));
+    adoc("3", ctx.makePoint(50.9289094, -32.7693246));
+    adoc("4", ctx.makePoint(60.9289094, -50.7693246));
+    adoc("5", ctx.makePoint(0, 0));
+    adoc("6", ctx.makePoint(0.1, 0.1));
+    adoc("7", ctx.makePoint(-0.1, -0.1));
+    adoc("8", ctx.makePoint(179.9, 0));
+    adoc("9", ctx.makePoint(-179.9, 0));
+    adoc("10", ctx.makePoint(50, 89.9));
+    adoc("11", ctx.makePoint(-130, 89.9));
+    adoc("12", ctx.makePoint(50, -89.9));
+    adoc("13", ctx.makePoint(-130, -89.9));
+    commit();
+  }
+
+
+  @Test
+  public void testIntersections() throws Exception {
+    setupDocs();
+    //Try some edge cases
+      //NOTE: 2nd arg is distance in kilometers
+    checkHitsCircle(ctx.makePoint(1, 1), 175, 3, 5, 6, 7);
+    checkHitsCircle(ctx.makePoint(179.8, 0), 200, 2, 8, 9);
+    checkHitsCircle(ctx.makePoint(50, 89.8), 200, 2, 10, 11);//this goes over the north pole
+    checkHitsCircle(ctx.makePoint(50, -89.8), 200, 2, 12, 13);//this goes over the south pole
+    //try some normal cases
+    checkHitsCircle(ctx.makePoint(-80.0, 33.0), 300, 2);
+    //large distance
+    checkHitsCircle(ctx.makePoint(1, 1), 5000, 3, 5, 6, 7);
+    //Because we are generating a box based on the west/east longitudes and the south/north latitudes, which then
+    //translates to a range query, which is slightly more inclusive.  Thus, even though 0.0 is 15.725 kms away,
+    //it will be included, b/c of the box calculation.
+    checkHitsBBox(ctx.makePoint(0.1, 0.1), 15, 2, 5, 6);
+    //try some more
+    deleteAll();
+    adoc("14", ctx.makePoint(5, 0));
+    adoc("15", ctx.makePoint(15, 0));
+    //3000KM from 0,0, see http://www.movable-type.co.uk/scripts/latlong.html
+    adoc("16", ctx.makePoint(19.79750, 18.71111));
+    adoc("17", ctx.makePoint(-95.436643, 44.043900));
+    commit();
+
+    checkHitsCircle(ctx.makePoint(0, 0), 1000, 1, 14);
+    checkHitsCircle(ctx.makePoint(0, 0), 2000, 2, 14, 15);
+    checkHitsBBox(ctx.makePoint(0, 0), 3000, 3, 14, 15, 16);
+    checkHitsCircle(ctx.makePoint(0, 0), 3001, 3, 14, 15, 16);
+    checkHitsCircle(ctx.makePoint(0, 0), 3000.1, 3, 14, 15, 16);
+
+    //really fine grained distance and reflects some of the vagaries of how we are calculating the box
+    checkHitsCircle(ctx.makePoint(-96.789603, 43.517030), 109, 0);
+
+    // falls outside of the real distance, but inside the bounding box
+    checkHitsCircle(ctx.makePoint(-96.789603, 43.517030), 110, 0);
+    checkHitsBBox(ctx.makePoint(-96.789603, 43.517030), 110, 1, 17);
+  }
+
+  //---- these are similar to Solr test methods
+
+  private void checkHitsCircle(Point pt, double distKM, int assertNumFound, int... assertIds) {
+    _checkHits(false, pt, distKM, assertNumFound, assertIds);
+  }
+  private void checkHitsBBox(Point pt, double distKM, int assertNumFound, int... assertIds) {
+    _checkHits(true, pt, distKM, assertNumFound, assertIds);
+  }
+
+  private void _checkHits(boolean bbox, Point pt, double distKM, int assertNumFound, int... assertIds) {
+    SpatialOperation op = SpatialOperation.Intersects;
+    double distDEG = DistanceUtils.dist2Degrees(distKM, DistanceUtils.EARTH_MEAN_RADIUS_KM);
+    Shape shape = ctx.makeCircle(pt, distDEG);
+    if (bbox)
+      shape = shape.getBoundingBox();
+
+    SpatialArgs args = new SpatialArgs(op,shape);
+    //args.setDistPrecision(0.025);
+    Query query = strategy.makeQuery(args);
+    SearchResults results = executeQuery(query, 100);
+    assertEquals(""+shape,assertNumFound,results.numFound);
+    if (assertIds != null) {
+      Set<Integer> resultIds = new HashSet<>();
+      for (SearchResult result : results.results) {
+        resultIds.add(Integer.valueOf(result.document.get("id")));
+      }
+      for (int assertId : assertIds) {
+        assertTrue("has " + assertId, resultIds.contains(assertId));
+      }
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/QueryEqualsHashCodeTest.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/QueryEqualsHashCodeTest.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/QueryEqualsHashCodeTest.java
new file mode 100644
index 0000000..b1a5e54
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/QueryEqualsHashCodeTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.spatial;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.spatial.bbox.BBoxStrategy;
+import org.apache.lucene.spatial.composite.CompositeSpatialStrategy;
+import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
+import org.apache.lucene.spatial.prefix.TermQueryPrefixTreeStrategy;
+import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
+import org.apache.lucene.spatial.query.SpatialArgs;
+import org.apache.lucene.spatial.query.SpatialOperation;
+import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
+import org.apache.lucene.spatial.vector.PointVectorStrategy;
+import org.apache.lucene.util.LuceneTestCase;
+import org.junit.Test;
+
+public class QueryEqualsHashCodeTest extends LuceneTestCase {
+
+  private final SpatialContext ctx = SpatialContext.GEO;
+
+  private SpatialOperation predicate;
+
+  @Test
+  public void testEqualsHashCode() {
+
+    switch (random().nextInt(4)) {//0-3
+      case 0: predicate = SpatialOperation.Contains; break;
+      case 1: predicate = SpatialOperation.IsWithin; break;
+
+      default: predicate = SpatialOperation.Intersects; break;
+    }
+    final SpatialPrefixTree gridQuad = new QuadPrefixTree(ctx,10);
+    final SpatialPrefixTree gridGeohash = new GeohashPrefixTree(ctx,10);
+
+    Collection<SpatialStrategy> strategies = new ArrayList<>();
+    RecursivePrefixTreeStrategy recursive_geohash = new RecursivePrefixTreeStrategy(gridGeohash, "recursive_geohash");
+    strategies.add(recursive_geohash);
+    strategies.add(new TermQueryPrefixTreeStrategy(gridQuad, "termquery_quad"));
+    strategies.add(new PointVectorStrategy(ctx, "pointvector"));
+    strategies.add(new BBoxStrategy(ctx, "bbox"));
+    final SerializedDVStrategy serialized = new SerializedDVStrategy(ctx, "serialized");
+    strategies.add(serialized);
+    strategies.add(new CompositeSpatialStrategy("composite", recursive_geohash, serialized));
+    for (SpatialStrategy strategy : strategies) {
+      testEqualsHashcode(strategy);
+    }
+  }
+
+  private void testEqualsHashcode(final SpatialStrategy strategy) {
+    final SpatialArgs args1 = makeArgs1();
+    final SpatialArgs args2 = makeArgs2();
+    testEqualsHashcode(args1, args2, new ObjGenerator() {
+      @Override
+      public Object gen(SpatialArgs args) {
+        return strategy.makeQuery(args);
+      }
+    });
+    testEqualsHashcode(args1, args2, new ObjGenerator() {
+      @Override
+      public Object gen(SpatialArgs args) {
+        return strategy.makeDistanceValueSource(args.getShape().getCenter());
+      }
+    });
+  }
+
+  private void testEqualsHashcode(SpatialArgs args1, SpatialArgs args2, ObjGenerator generator) {
+    Object first;
+    try {
+      first = generator.gen(args1);
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    if (first == null)
+      return;//unsupported op?
+    Object second = generator.gen(args1);//should be the same
+    assertEquals(first, second);
+    assertEquals(first.hashCode(), second.hashCode());
+    assertTrue(args1.equals(args2) == false);
+    second = generator.gen(args2);//now should be different
+    assertTrue(first.equals(second) == false);
+    assertTrue(first.hashCode() != second.hashCode());
+  }
+
+  private SpatialArgs makeArgs1() {
+    final Shape shape1 = ctx.makeRectangle(0, 0, 10, 10);
+    return new SpatialArgs(predicate, shape1);
+  }
+
+  private SpatialArgs makeArgs2() {
+    final Shape shape2 = ctx.makeRectangle(0, 0, 20, 20);
+    return new SpatialArgs(predicate, shape2);
+  }
+
+  interface ObjGenerator {
+    Object gen(SpatialArgs args);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialArgsTest.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialArgsTest.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialArgsTest.java
new file mode 100644
index 0000000..09b5d46
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialArgsTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.spatial;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.spatial.query.SpatialArgs;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class SpatialArgsTest {
+
+  @Test
+  public void calcDistanceFromErrPct() {
+    final SpatialContext ctx = SpatialContext.GEO;
+    final double DEP = 0.5;//distErrPct
+
+    //the result is the diagonal distance from the center to the closest corner,
+    // times distErrPct
+
+    Shape superwide = ctx.makeRectangle(-180, 180, 0, 0);
+    //0 distErrPct means 0 distance always
+    assertEquals(0, SpatialArgs.calcDistanceFromErrPct(superwide, 0, ctx), 0);
+    assertEquals(180 * DEP, SpatialArgs.calcDistanceFromErrPct(superwide, DEP, ctx), 0);
+
+    Shape supertall = ctx.makeRectangle(0, 0, -90, 90);
+    assertEquals(90 * DEP, SpatialArgs.calcDistanceFromErrPct(supertall, DEP, ctx), 0);
+
+    Shape upperhalf = ctx.makeRectangle(-180, 180, 0, 90);
+    assertEquals(45 * DEP, SpatialArgs.calcDistanceFromErrPct(upperhalf, DEP, ctx), 0.0001);
+
+    Shape midCircle = ctx.makeCircle(0, 0, 45);
+    assertEquals(60 * DEP, SpatialArgs.calcDistanceFromErrPct(midCircle, DEP, ctx), 0.0001);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialExample.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialExample.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialExample.java
new file mode 100644
index 0000000..1bd7159
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialExample.java
@@ -0,0 +1,200 @@
+/*
+ * 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.spatial;
+
+import java.io.IOException;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.distance.DistanceUtils;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.NumericDocValuesField;
+import org.apache.lucene.document.StoredField;
+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.queries.function.ValueSource;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
+import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
+import org.apache.lucene.spatial.query.SpatialArgs;
+import org.apache.lucene.spatial.query.SpatialArgsParser;
+import org.apache.lucene.spatial.query.SpatialOperation;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.RAMDirectory;
+import org.apache.lucene.util.LuceneTestCase;
+
+/**
+ * This class serves as example code to show how to use the Lucene spatial
+ * module.
+ */
+public class SpatialExample extends LuceneTestCase {
+
+  //Note: Test invoked via TestTestFramework.spatialExample()
+
+  public static void main(String[] args) throws Exception {
+    new SpatialExample().test();
+  }
+
+  public void test() throws Exception {
+    init();
+    indexPoints();
+    search();
+  }
+
+  /**
+   * The Spatial4j {@link SpatialContext} is a sort of global-ish singleton
+   * needed by Lucene spatial.  It's a facade to the rest of Spatial4j, acting
+   * as a factory for {@link Shape}s and provides access to reading and writing
+   * them from Strings.
+   */
+  private SpatialContext ctx;//"ctx" is the conventional variable name
+
+  /**
+   * The Lucene spatial {@link SpatialStrategy} encapsulates an approach to
+   * indexing and searching shapes, and providing distance values for them.
+   * It's a simple API to unify different approaches. You might use more than
+   * one strategy for a shape as each strategy has its strengths and weaknesses.
+   * <p />
+   * Note that these are initialized with a field name.
+   */
+  private SpatialStrategy strategy;
+
+  private Directory directory;
+
+  protected void init() {
+    //Typical geospatial context
+    //  These can also be constructed from SpatialContextFactory
+    this.ctx = SpatialContext.GEO;
+
+    int maxLevels = 11;//results in sub-meter precision for geohash
+    //TODO demo lookup by detail distance
+    //  This can also be constructed from SpatialPrefixTreeFactory
+    SpatialPrefixTree grid = new GeohashPrefixTree(ctx, maxLevels);
+
+    this.strategy = new RecursivePrefixTreeStrategy(grid, "myGeoField");
+
+    this.directory = new RAMDirectory();
+  }
+
+  private void indexPoints() throws Exception {
+    IndexWriterConfig iwConfig = new IndexWriterConfig(null);
+    IndexWriter indexWriter = new IndexWriter(directory, iwConfig);
+
+    //Spatial4j is x-y order for arguments
+    indexWriter.addDocument(newSampleDocument(
+        2, ctx.makePoint(-80.93, 33.77)));
+
+    //Spatial4j has a WKT parser which is also "x y" order
+    indexWriter.addDocument(newSampleDocument(
+        4, ctx.readShapeFromWkt("POINT(60.9289094 -50.7693246)")));
+
+    indexWriter.addDocument(newSampleDocument(
+        20, ctx.makePoint(0.1,0.1), ctx.makePoint(0, 0)));
+
+    indexWriter.close();
+  }
+
+  private Document newSampleDocument(int id, Shape... shapes) {
+    Document doc = new Document();
+    doc.add(new StoredField("id", id));
+    doc.add(new NumericDocValuesField("id", id));
+    //Potentially more than one shape in this field is supported by some
+    // strategies; see the javadocs of the SpatialStrategy impl to see.
+    for (Shape shape : shapes) {
+      for (Field f : strategy.createIndexableFields(shape)) {
+        doc.add(f);
+      }
+      //store it too; the format is up to you
+      //  (assume point in this example)
+      Point pt = (Point) shape;
+      doc.add(new StoredField(strategy.getFieldName(), pt.getX()+" "+pt.getY()));
+    }
+
+    return doc;
+  }
+
+  private void search() throws Exception {
+    IndexReader indexReader = DirectoryReader.open(directory);
+    IndexSearcher indexSearcher = new IndexSearcher(indexReader);
+    Sort idSort = new Sort(new SortField("id", SortField.Type.INT));
+
+    //--Filter by circle (<= distance from a point)
+    {
+      //Search with circle
+      //note: SpatialArgs can be parsed from a string
+      SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects,
+          ctx.makeCircle(-80.0, 33.0, DistanceUtils.dist2Degrees(200, DistanceUtils.EARTH_MEAN_RADIUS_KM)));
+      Query query = strategy.makeQuery(args);
+      TopDocs docs = indexSearcher.search(query, 10, idSort);
+      assertDocMatchedIds(indexSearcher, docs, 2);
+      //Now, lets get the distance for the 1st doc via computing from stored point value:
+      // (this computation is usually not redundant)
+      Document doc1 = indexSearcher.doc(docs.scoreDocs[0].doc);
+      String doc1Str = doc1.getField(strategy.getFieldName()).stringValue();
+      //assume doc1Str is "x y" as written in newSampleDocument()
+      int spaceIdx = doc1Str.indexOf(' ');
+      double x = Double.parseDouble(doc1Str.substring(0, spaceIdx));
+      double y = Double.parseDouble(doc1Str.substring(spaceIdx+1));
+      double doc1DistDEG = ctx.calcDistance(args.getShape().getCenter(), x, y);
+      assertEquals(121.6d, DistanceUtils.degrees2Dist(doc1DistDEG, DistanceUtils.EARTH_MEAN_RADIUS_KM), 0.1);
+      //or more simply:
+      assertEquals(121.6d, doc1DistDEG * DistanceUtils.DEG_TO_KM, 0.1);
+    }
+    //--Match all, order by distance ascending
+    {
+      Point pt = ctx.makePoint(60, -50);
+      ValueSource valueSource = strategy.makeDistanceValueSource(pt, DistanceUtils.DEG_TO_KM);//the distance (in km)
+      Sort distSort = new Sort(valueSource.getSortField(false)).rewrite(indexSearcher);//false=asc dist
+      TopDocs docs = indexSearcher.search(new MatchAllDocsQuery(), 10, distSort);
+      assertDocMatchedIds(indexSearcher, docs, 4, 20, 2);
+      //To get the distance, we could compute from stored values like earlier.
+      // However in this example we sorted on it, and the distance will get
+      // computed redundantly.  If the distance is only needed for the top-X
+      // search results then that's not a big deal. Alternatively, try wrapping
+      // the ValueSource with CachingDoubleValueSource then retrieve the value
+      // from the ValueSource now. See LUCENE-4541 for an example.
+    }
+    //demo arg parsing
+    {
+      SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects,
+          ctx.makeCircle(-80.0, 33.0, 1));
+      SpatialArgs args2 = new SpatialArgsParser().parse("Intersects(BUFFER(POINT(-80 33),1))", ctx);
+      assertEquals(args.toString(),args2.toString());
+    }
+
+    indexReader.close();
+  }
+
+  private void assertDocMatchedIds(IndexSearcher indexSearcher, TopDocs docs, int... ids) throws IOException {
+    int[] gotIds = new int[docs.totalHits];
+    for (int i = 0; i < gotIds.length; i++) {
+      gotIds[i] = indexSearcher.doc(docs.scoreDocs[i].doc).getField("id").numericValue().intValue();
+    }
+    assertArrayEquals(ids,gotIds);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialMatchConcern.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialMatchConcern.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialMatchConcern.java
new file mode 100644
index 0000000..e995ee1
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialMatchConcern.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.spatial;
+
+public class SpatialMatchConcern {
+  public final boolean orderIsImportant;
+  public final boolean resultsAreSuperset; // if the strategy can not give exact answers, but used to limit results
+
+  private SpatialMatchConcern( boolean order, boolean superset ) {
+    this.orderIsImportant = order;
+    this.resultsAreSuperset = superset;
+  }
+
+  public static final SpatialMatchConcern EXACT = new SpatialMatchConcern( true, false );
+  public static final SpatialMatchConcern FILTER = new SpatialMatchConcern( false, false );
+  public static final SpatialMatchConcern SUPERSET = new SpatialMatchConcern( false, true );
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestCase.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestCase.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestCase.java
new file mode 100644
index 0000000..94e5a8e
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestCase.java
@@ -0,0 +1,280 @@
+/*
+ * 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.spatial;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.logging.Logger;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.distance.DistanceUtils;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Rectangle;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.MockAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.uninverting.UninvertingReader;
+import org.apache.lucene.uninverting.UninvertingReader.Type;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks;
+import org.apache.lucene.util.TestUtil;
+
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomDouble;
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomGaussian;
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomInt;
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
+
+/** A base test class for spatial lucene. It's mostly Lucene generic. */
+@SuppressSysoutChecks(bugUrl = "These tests use JUL extensively.")
+public abstract class SpatialTestCase extends LuceneTestCase {
+
+  protected Logger log = Logger.getLogger(getClass().getName());
+
+  private DirectoryReader indexReader;
+  protected RandomIndexWriter indexWriter;
+  private Directory directory;
+  private Analyzer analyzer;
+  protected IndexSearcher indexSearcher;
+
+  protected SpatialContext ctx;//subclass must initialize
+
+  protected Map<String,Type> uninvertMap = new HashMap<>();
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    // TODO: change this module to index docvalues instead of uninverting
+    uninvertMap.clear();
+    uninvertMap.put("pointvector__x", Type.DOUBLE);
+    uninvertMap.put("pointvector__y", Type.DOUBLE);
+
+    directory = newDirectory();
+    final Random random = random();
+    analyzer = new MockAnalyzer(random);
+    indexWriter = new RandomIndexWriter(random,directory, newIWConfig(random, analyzer));
+    indexReader = UninvertingReader.wrap(indexWriter.getReader(), uninvertMap);
+    indexSearcher = newSearcher(indexReader);
+  }
+
+  protected IndexWriterConfig newIWConfig(Random random, Analyzer analyzer) {
+    final IndexWriterConfig indexWriterConfig = LuceneTestCase.newIndexWriterConfig(random, analyzer);
+    //TODO can we randomly choose a doc-values supported format?
+    if (needsDocValues())
+      indexWriterConfig.setCodec( TestUtil.getDefaultCodec());
+    return indexWriterConfig;
+  }
+
+  protected boolean needsDocValues() {
+    return false;
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    IOUtils.close(indexWriter, indexReader, analyzer, directory);
+    super.tearDown();
+  }
+
+  // ================================================= Helper Methods ================================================
+
+  protected void addDocument(Document doc) throws IOException {
+    indexWriter.addDocument(doc);
+  }
+
+  protected void addDocumentsAndCommit(List<Document> documents) throws IOException {
+    for (Document document : documents) {
+      indexWriter.addDocument(document);
+    }
+    commit();
+  }
+
+  protected void deleteAll() throws IOException {
+    indexWriter.deleteAll();
+  }
+
+  protected void commit() throws IOException {
+    indexWriter.commit();
+    DirectoryReader newReader = DirectoryReader.openIfChanged(indexReader);
+    if (newReader != null) {
+      IOUtils.close(indexReader);
+      indexReader = newReader;
+    }
+    indexSearcher = newSearcher(indexReader);
+  }
+
+  protected void verifyDocumentsIndexed(int numDocs) {
+    assertEquals(numDocs, indexReader.numDocs());
+  }
+
+  protected SearchResults executeQuery(Query query, int numDocs) {
+    try {
+      TopDocs topDocs = indexSearcher.search(query, numDocs);
+
+      List<SearchResult> results = new ArrayList<>();
+      for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
+        results.add(new SearchResult(scoreDoc.score, indexSearcher.doc(scoreDoc.doc)));
+      }
+      return new SearchResults(topDocs.totalHits, results);
+    } catch (IOException ioe) {
+      throw new RuntimeException("IOException thrown while executing query", ioe);
+    }
+  }
+
+  protected Point randomPoint() {
+    final Rectangle WB = ctx.getWorldBounds();
+    return ctx.makePoint(
+        randomIntBetween((int) WB.getMinX(), (int) WB.getMaxX()),
+        randomIntBetween((int) WB.getMinY(), (int) WB.getMaxY()));
+  }
+
+  protected Rectangle randomRectangle() {
+    return randomRectangle(ctx.getWorldBounds());
+  }
+
+  protected Rectangle randomRectangle(Rectangle bounds) {
+    double[] xNewStartAndWidth = randomSubRange(bounds.getMinX(), bounds.getWidth());
+    double xMin = xNewStartAndWidth[0];
+    double xMax = xMin + xNewStartAndWidth[1];
+    if (bounds.getCrossesDateLine()) {
+      xMin = DistanceUtils.normLonDEG(xMin);
+      xMax = DistanceUtils.normLonDEG(xMax);
+    }
+
+    double[] yNewStartAndHeight = randomSubRange(bounds.getMinY(), bounds.getHeight());
+    double yMin = yNewStartAndHeight[0];
+    double yMax = yMin + yNewStartAndHeight[1];
+
+    return ctx.makeRectangle(xMin, xMax, yMin, yMax);
+  }
+
+  /** Returns new minStart and new length that is inside the range specified by the arguments. */
+  protected double[] randomSubRange(double boundStart, double boundLen) {
+    if (boundLen >= 3 && usually()) { // typical
+      // prefer integers for ease of debugability ... and prefer 1/16th of bound
+      int intBoundStart = (int) Math.ceil(boundStart);
+      int intBoundEnd = (int) (boundStart + boundLen);
+      int intBoundLen = intBoundEnd - intBoundStart;
+      int newLen = (int) randomGaussianMeanMax(intBoundLen / 16.0, intBoundLen);
+      int newStart = intBoundStart + randomInt(intBoundLen - newLen);
+      return new double[]{newStart, newLen};
+    } else { // (no int rounding)
+      double newLen = randomGaussianMeanMax(boundLen / 16, boundLen);
+      double newStart = boundStart + (boundLen - newLen == 0 ? 0 : (randomDouble() % (boundLen - newLen)));
+      return new double[]{newStart, newLen};
+    }
+  }
+
+  private double randomGaussianMinMeanMax(double min, double mean, double max) {
+    assert mean > min;
+    return randomGaussianMeanMax(mean - min, max - min) + min;
+  }
+
+  /**
+   * Within one standard deviation (68% of the time) the result is "close" to
+   * mean. By "close": when greater than mean, it's the lesser of 2*mean or half
+   * way to max, when lesser than mean, it's the greater of max-2*mean or half
+   * way to 0. The other 32% of the time it's in the rest of the range, touching
+   * either 0 or max but never exceeding.
+   */
+  private double randomGaussianMeanMax(double mean, double max) {
+    // DWS: I verified the results empirically
+    assert mean <= max && mean >= 0;
+    double g = randomGaussian();
+    double mean2 = mean;
+    double flip = 1;
+    if (g < 0) {
+      mean2 = max - mean;
+      flip = -1;
+      g *= -1;
+    }
+    // pivot is the distance from mean2 towards max where the boundary of
+    // 1 standard deviation alters the calculation
+    double pivotMax = max - mean2;
+    double pivot = Math.min(mean2, pivotMax / 2);//from 0 to max-mean2
+    assert pivot >= 0 && pivotMax >= pivot && g >= 0;
+    double pivotResult;
+    if (g <= 1)
+      pivotResult = pivot * g;
+    else
+      pivotResult = Math.min(pivotMax, (g - 1) * (pivotMax - pivot) + pivot);
+
+    double result = mean + flip * pivotResult;
+    return (result < 0 || result > max) ? mean : result; // due this due to computational numerical precision
+  }
+
+  // ================================================= Inner Classes =================================================
+
+  protected static class SearchResults {
+
+    public int numFound;
+    public List<SearchResult> results;
+
+    public SearchResults(int numFound, List<SearchResult> results) {
+      this.numFound = numFound;
+      this.results = results;
+    }
+
+    public StringBuilder toDebugString() {
+      StringBuilder str = new StringBuilder();
+      str.append("found: ").append(numFound).append('[');
+      for(SearchResult r : results) {
+        String id = r.getId();
+        str.append(id).append(", ");
+      }
+      str.append(']');
+      return str;
+    }
+
+    @Override
+    public String toString() {
+      return "[found:"+numFound+" "+results+"]";
+    }
+  }
+
+  protected static class SearchResult {
+
+    public float score;
+    public Document document;
+
+    public SearchResult(float score, Document document) {
+      this.score = score;
+      this.document = document;
+    }
+
+    public String getId() {
+      return document.get("id");
+    }
+
+    @Override
+    public String toString() {
+      return "["+score+"="+document+"]";
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestData.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestData.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestData.java
new file mode 100644
index 0000000..27d47b3
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestData.java
@@ -0,0 +1,71 @@
+/*
+ * 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.spatial;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Shape;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+// This class is modelled after SpatialTestQuery.
+// Before Lucene 4.7, this was a bit different in Spatial4j as SampleData & SampleDataReader.
+
+public class SpatialTestData {
+  public String id;
+  public String name;
+  public Shape shape;
+
+  /** Reads the stream, consuming a format that is a tab-separated values of 3 columns:
+   * an "id", a "name" and the "shape".  Empty lines and lines starting with a '#' are skipped.
+   * The stream is closed.
+   */
+  public static Iterator<SpatialTestData> getTestData(InputStream in, SpatialContext ctx) throws IOException {
+    List<SpatialTestData> results = new ArrayList<>();
+    BufferedReader bufInput = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+    try {
+      String line;
+      while ((line = bufInput.readLine()) != null) {
+        if (line.length() == 0 || line.charAt(0) == '#')
+          continue;
+
+        SpatialTestData data = new SpatialTestData();
+        String[] vals = line.split("\t");
+        if (vals.length != 3)
+          throw new RuntimeException("bad format; expecting 3 tab-separated values for line: "+line);
+        data.id = vals[0];
+        data.name = vals[1];
+        try {
+          data.shape = ctx.readShapeFromWkt(vals[2]);
+        } catch (ParseException e) {
+          throw new RuntimeException(e);
+        }
+        results.add(data);
+      }
+    } finally {
+      bufInput.close();
+    }
+    return results.iterator();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestQuery.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestQuery.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestQuery.java
new file mode 100644
index 0000000..bac90cf
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/SpatialTestQuery.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.spatial;
+
+import com.spatial4j.core.context.SpatialContext;
+
+import org.apache.lucene.spatial.query.SpatialArgs;
+import org.apache.lucene.spatial.query.SpatialArgsParser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Helper class to execute queries
+ */
+public class SpatialTestQuery {
+  public String testname;
+  public String line;
+  public int lineNumber = -1;
+  public SpatialArgs args;
+  public List<String> ids = new ArrayList<>();
+
+  /**
+   * Get Test Queries.  The InputStream is closed.
+   */
+  public static Iterator<SpatialTestQuery> getTestQueries(
+      final SpatialArgsParser parser,
+      final SpatialContext ctx,
+      final String name,
+      final InputStream in ) throws IOException {
+
+    List<SpatialTestQuery> results = new ArrayList<>();
+
+    BufferedReader bufInput = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+    try {
+      String line;
+      for (int lineNumber = 1; (line = bufInput.readLine()) != null; lineNumber++) {
+        SpatialTestQuery test = new SpatialTestQuery();
+        test.line = line;
+        test.lineNumber = lineNumber;
+
+        try {
+          // skip a comment
+          if( line.startsWith( "[" ) ) {
+            int idx = line.indexOf( ']' );
+            if( idx > 0 ) {
+              line = line.substring( idx+1 );
+            }
+          }
+
+          int idx = line.indexOf('@');
+          StringTokenizer st = new StringTokenizer(line.substring(0, idx));
+          while (st.hasMoreTokens()) {
+            test.ids.add(st.nextToken().trim());
+          }
+          test.args = parser.parse(line.substring(idx + 1).trim(), ctx);
+          results.add(test);
+        }
+        catch( Exception ex ) {
+          throw new RuntimeException( "invalid query line: "+test.line, ex );
+        }
+      }
+    } finally {
+      bufInput.close();
+    }
+    return results.iterator();
+  }
+
+  @Override
+  public String toString() {
+    if (line != null)
+      return line;
+    return args.toString()+" "+ids;
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/StrategyTestCase.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/StrategyTestCase.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/StrategyTestCase.java
new file mode 100644
index 0000000..00e437b
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/StrategyTestCase.java
@@ -0,0 +1,252 @@
+/*
+ * 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.spatial;
+
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Shape;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.queries.function.FunctionQuery;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.CheckHits;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.spatial.query.SpatialArgs;
+import org.apache.lucene.spatial.query.SpatialArgsParser;
+import org.apache.lucene.spatial.query.SpatialOperation;
+
+public abstract class StrategyTestCase extends SpatialTestCase {
+
+  public static final String DATA_SIMPLE_BBOX = "simple-bbox.txt";
+  public static final String DATA_STATES_POLY = "states-poly.txt";
+  public static final String DATA_STATES_BBOX = "states-bbox.txt";
+  public static final String DATA_COUNTRIES_POLY = "countries-poly.txt";
+  public static final String DATA_COUNTRIES_BBOX = "countries-bbox.txt";
+  public static final String DATA_WORLD_CITIES_POINTS = "world-cities-points.txt";
+
+  public static final String QTEST_States_IsWithin_BBox   = "states-IsWithin-BBox.txt";
+  public static final String QTEST_States_Intersects_BBox = "states-Intersects-BBox.txt";
+  public static final String QTEST_Cities_Intersects_BBox = "cities-Intersects-BBox.txt";
+  public static final String QTEST_Simple_Queries_BBox = "simple-Queries-BBox.txt";
+
+  protected Logger log = Logger.getLogger(getClass().getName());
+
+  protected final SpatialArgsParser argsParser = new SpatialArgsParser();
+
+  protected SpatialStrategy strategy;
+  protected boolean storeShape = true;
+
+  protected void executeQueries(SpatialMatchConcern concern, String... testQueryFile) throws IOException {
+    log.info("testing queried for strategy "+strategy);
+    for( String path : testQueryFile ) {
+      Iterator<SpatialTestQuery> testQueryIterator = getTestQueries(path, ctx);
+      runTestQueries(testQueryIterator, concern);
+    }
+  }
+
+  protected void getAddAndVerifyIndexedDocuments(String testDataFile) throws IOException {
+    List<Document> testDocuments = getDocuments(testDataFile);
+    addDocumentsAndCommit(testDocuments);
+    verifyDocumentsIndexed(testDocuments.size());
+  }
+
+  protected List<Document> getDocuments(String testDataFile) throws IOException {
+    return getDocuments(getSampleData(testDataFile));
+  }
+
+  protected List<Document> getDocuments(Iterator<SpatialTestData> sampleData) {
+    List<Document> documents = new ArrayList<>();
+    while (sampleData.hasNext()) {
+      SpatialTestData data = sampleData.next();
+      Document document = new Document();
+      document.add(new StringField("id", data.id, Field.Store.YES));
+      document.add(new StringField("name", data.name, Field.Store.YES));
+      Shape shape = data.shape;
+      shape = convertShapeFromGetDocuments(shape);
+      if (shape != null) {
+        for (Field f : strategy.createIndexableFields(shape)) {
+          document.add(f);
+        }
+        if (storeShape)//just for diagnostics
+          document.add(new StoredField(strategy.getFieldName(), shape.toString()));
+      }
+
+      documents.add(document);
+    }
+    return documents;
+  }
+
+  /** Subclasses may override to transform or remove a shape for indexing */
+  protected Shape convertShapeFromGetDocuments(Shape shape) {
+    return shape;
+  }
+
+  protected Iterator<SpatialTestData> getSampleData(String testDataFile) throws IOException {
+    String path = "data/" + testDataFile;
+    InputStream stream = getClass().getClassLoader().getResourceAsStream(path);
+    if (stream == null)
+      throw new FileNotFoundException("classpath resource not found: "+path);
+    return SpatialTestData.getTestData(stream, ctx);//closes the InputStream
+  }
+
+  protected Iterator<SpatialTestQuery> getTestQueries(String testQueryFile, SpatialContext ctx) throws IOException {
+    InputStream in = getClass().getClassLoader().getResourceAsStream(testQueryFile);
+    return SpatialTestQuery.getTestQueries(
+        argsParser, ctx, testQueryFile, in );//closes the InputStream
+  }
+
+  public void runTestQueries(
+      Iterator<SpatialTestQuery> queries,
+      SpatialMatchConcern concern) {
+    while (queries.hasNext()) {
+      SpatialTestQuery q = queries.next();
+      runTestQuery(concern, q);
+    }
+  }
+
+  public void runTestQuery(SpatialMatchConcern concern, SpatialTestQuery q) {
+    String msg = q.toString(); //"Query: " + q.args.toString(ctx);
+    SearchResults got = executeQuery(makeQuery(q), Math.max(100, q.ids.size()+1));
+    if (storeShape && got.numFound > 0) {
+      //check stored value is there
+      assertNotNull(got.results.get(0).document.get(strategy.getFieldName()));
+    }
+    if (concern.orderIsImportant) {
+      Iterator<String> ids = q.ids.iterator();
+      for (SearchResult r : got.results) {
+        String id = r.document.get("id");
+        if (!ids.hasNext()) {
+          fail(msg + " :: Did not get enough results.  Expect" + q.ids + ", got: " + got.toDebugString());
+        }
+        assertEquals("out of order: " + msg, ids.next(), id);
+      }
+
+      if (ids.hasNext()) {
+        fail(msg + " :: expect more results then we got: " + ids.next());
+      }
+    } else {
+      // We are looking at how the results overlap
+      if (concern.resultsAreSuperset) {
+        Set<String> found = new HashSet<>();
+        for (SearchResult r : got.results) {
+          found.add(r.document.get("id"));
+        }
+        for (String s : q.ids) {
+          if (!found.contains(s)) {
+            fail("Results are mising id: " + s + " :: " + found);
+          }
+        }
+      } else {
+        List<String> found = new ArrayList<>();
+        for (SearchResult r : got.results) {
+          found.add(r.document.get("id"));
+        }
+
+        // sort both so that the order is not important
+        Collections.sort(q.ids);
+        Collections.sort(found);
+        assertEquals(msg, q.ids.toString(), found.toString());
+      }
+    }
+  }
+
+  protected Query makeQuery(SpatialTestQuery q) {
+    return strategy.makeQuery(q.args);
+  }
+
+  protected void adoc(String id, String shapeStr) throws IOException, ParseException {
+    Shape shape = shapeStr==null ? null : ctx.readShapeFromWkt(shapeStr);
+    addDocument(newDoc(id, shape));
+  }
+  protected void adoc(String id, Shape shape) throws IOException {
+    addDocument(newDoc(id, shape));
+  }
+
+  protected Document newDoc(String id, Shape shape) {
+    Document doc = new Document();
+    doc.add(new StringField("id", id, Field.Store.YES));
+    if (shape != null) {
+      for (Field f : strategy.createIndexableFields(shape)) {
+        doc.add(f);
+      }
+      if (storeShape)
+        doc.add(new StoredField(strategy.getFieldName(), shape.toString()));//not to be parsed; just for debug
+    }
+    return doc;
+  }
+
+  protected void deleteDoc(String id) throws IOException {
+    indexWriter.deleteDocuments(new TermQuery(new Term("id", id)));
+  }
+
+  /** scores[] are in docId order */
+  protected void checkValueSource(ValueSource vs, float scores[], float delta) throws IOException {
+    FunctionQuery q = new FunctionQuery(vs);
+
+//    //TODO is there any point to this check?
+//    int expectedDocs[] = new int[scores.length];//fill with ascending 0....length-1
+//    for (int i = 0; i < expectedDocs.length; i++) {
+//      expectedDocs[i] = i;
+//    }
+//    CheckHits.checkHits(random(), q, "", indexSearcher, expectedDocs);
+
+    //TopDocs is sorted but we actually don't care about the order
+    TopDocs docs = indexSearcher.search(q, 1000);//calculates the score
+    for (int i = 0; i < docs.scoreDocs.length; i++) {
+      ScoreDoc gotSD = docs.scoreDocs[i];
+      float expectedScore = scores[gotSD.doc];
+      assertEquals("Not equal for doc "+gotSD.doc, expectedScore, gotSD.score, delta);
+    }
+
+    CheckHits.checkExplanations(q, "", indexSearcher);
+  }
+
+  protected void testOperation(Shape indexedShape, SpatialOperation operation,
+                               Shape queryShape, boolean match) throws IOException {
+    assertTrue("Faulty test",
+        operation.evaluate(indexedShape, queryShape) == match ||
+            indexedShape.equals(queryShape) &&
+              (operation == SpatialOperation.Contains || operation == SpatialOperation.IsWithin));
+    adoc("0", indexedShape);
+    commit();
+    Query query = strategy.makeQuery(new SpatialArgs(operation, queryShape));
+    SearchResults got = executeQuery(query, 1);
+    assert got.numFound <= 1 : "unclean test env";
+    if ((got.numFound == 1) != match)
+      fail(operation+" I:" + indexedShape + " Q:" + queryShape);
+    deleteAll();//clean up after ourselves
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/TestTestFramework.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/TestTestFramework.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/TestTestFramework.java
new file mode 100644
index 0000000..6a8d59c
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/TestTestFramework.java
@@ -0,0 +1,68 @@
+/*
+ * 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.spatial;
+
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.shape.Rectangle;
+import org.apache.lucene.spatial.query.SpatialArgsParser;
+import org.apache.lucene.spatial.query.SpatialOperation;
+import org.apache.lucene.util.LuceneTestCase;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Make sure we are reading the tests as expected
+ */
+public class TestTestFramework extends LuceneTestCase {
+
+  @Test
+  public void testQueries() throws IOException {
+    String name = StrategyTestCase.QTEST_Cities_Intersects_BBox;
+
+    InputStream in = getClass().getClassLoader().getResourceAsStream(name);
+    SpatialContext ctx = SpatialContext.GEO;
+    Iterator<SpatialTestQuery> iter = SpatialTestQuery.getTestQueries(
+        new SpatialArgsParser(), ctx, name, in );//closes the InputStream
+    List<SpatialTestQuery> tests = new ArrayList<>();
+    while( iter.hasNext() ) {
+      tests.add( iter.next() );
+    }
+    Assert.assertEquals( 3, tests.size() );
+
+    SpatialTestQuery sf = tests.get(0);
+    // assert
+    Assert.assertEquals( 1, sf.ids.size() );
+    Assert.assertTrue( sf.ids.get(0).equals( "G5391959" ) );
+    Assert.assertTrue( sf.args.getShape() instanceof Rectangle);
+    Assert.assertEquals(SpatialOperation.Intersects, sf.args.getOperation());
+  }
+
+  @Test
+  public void spatialExample() throws Exception {
+    //kind of a hack so that SpatialExample is tested despite
+    // it not starting or ending with "Test".
+    SpatialExample.main(null);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java
new file mode 100644
index 0000000..6140996
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/bbox/TestBBoxStrategy.java
@@ -0,0 +1,301 @@
+/*
+ * 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.spatial.bbox;
+
+import java.io.IOException;
+
+import com.carrotsearch.randomizedtesting.annotations.Repeat;
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.context.SpatialContextFactory;
+import com.spatial4j.core.distance.DistanceUtils;
+import com.spatial4j.core.shape.Rectangle;
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.impl.RectangleImpl;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.index.DocValuesType;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.spatial.SpatialMatchConcern;
+import org.apache.lucene.spatial.prefix.RandomSpatialOpStrategyTestCase;
+import org.apache.lucene.spatial.query.SpatialArgs;
+import org.apache.lucene.spatial.query.SpatialOperation;
+import org.apache.lucene.spatial.util.ShapeAreaValueSource;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class TestBBoxStrategy extends RandomSpatialOpStrategyTestCase {
+
+  @Override
+  protected boolean needsDocValues() {
+    return true;
+  }
+
+  @Override
+  protected Shape randomIndexedShape() {
+    Rectangle world = ctx.getWorldBounds();
+    if (random().nextInt(10) == 0) // increased chance of getting one of these
+      return world;
+
+    int worldWidth = (int) Math.round(world.getWidth());
+    int deltaLeft = nextIntInclusive(worldWidth);
+    int deltaRight = nextIntInclusive(worldWidth - deltaLeft);
+    int worldHeight = (int) Math.round(world.getHeight());
+    int deltaTop = nextIntInclusive(worldHeight);
+    int deltaBottom = nextIntInclusive(worldHeight - deltaTop);
+    if (ctx.isGeo() && (deltaLeft != 0 || deltaRight != 0)) {
+      //if geo & doesn't world-wrap, we shift randomly to potentially cross dateline
+      int shift = nextIntInclusive(360);
+      return ctx.makeRectangle(
+          DistanceUtils.normLonDEG(world.getMinX() + deltaLeft + shift),
+          DistanceUtils.normLonDEG(world.getMaxX() - deltaRight + shift),
+          world.getMinY() + deltaBottom, world.getMaxY() - deltaTop);
+    } else {
+      return ctx.makeRectangle(
+          world.getMinX() + deltaLeft, world.getMaxX() - deltaRight,
+          world.getMinY() + deltaBottom, world.getMaxY() - deltaTop);
+    }
+  }
+
+  /** next int, inclusive, rounds to multiple of 10 if given evenly divisible. */
+  private int nextIntInclusive(int toInc) {
+    final int DIVIS = 10;
+    if (toInc % DIVIS == 0) {
+      return random().nextInt(toInc/DIVIS + 1) * DIVIS;
+    } else {
+      return random().nextInt(toInc + 1);
+    }
+  }
+
+  @Override
+  protected Shape randomQueryShape() {
+    return randomIndexedShape();
+  }
+
+  @Test
+  @Repeat(iterations = 15)
+  public void testOperations() throws IOException {
+    //setup
+    if (random().nextInt(4) > 0) {//75% of the time choose geo (more interesting to test)
+      this.ctx = SpatialContext.GEO;
+    } else {
+      SpatialContextFactory factory = new SpatialContextFactory();
+      factory.geo = false;
+      factory.worldBounds = new RectangleImpl(-300, 300, -100, 100, null);
+      this.ctx = factory.newSpatialContext();
+    }
+    this.strategy = new BBoxStrategy(ctx, "bbox");
+    //test we can disable docValues for predicate tests
+    if (random().nextBoolean()) {
+      BBoxStrategy bboxStrategy = (BBoxStrategy) strategy;
+      FieldType fieldType = new FieldType(bboxStrategy.getFieldType());
+      fieldType.setDocValuesType(DocValuesType.NONE);
+      bboxStrategy.setFieldType(fieldType);
+    }
+    for (SpatialOperation operation : SpatialOperation.values()) {
+      if (operation == SpatialOperation.Overlaps)
+        continue;//unsupported
+      testOperationRandomShapes(operation);
+
+      deleteAll();
+      commit();
+    }
+  }
+
+  @Test
+  public void testIntersectsBugDatelineEdge() throws IOException {
+    setupGeo();
+    testOperation(
+        ctx.makeRectangle(160, 180, -10, 10),
+        SpatialOperation.Intersects,
+        ctx.makeRectangle(-180, -160, -10, 10), true);
+  }
+
+  @Test
+  public void testIntersectsWorldDatelineEdge() throws IOException {
+    setupGeo();
+    testOperation(
+        ctx.makeRectangle(-180, 180, -10, 10),
+        SpatialOperation.Intersects,
+        ctx.makeRectangle(180, 180, -10, 10), true);
+  }
+
+  @Test
+  public void testWithinBugDatelineEdge() throws IOException {
+    setupGeo();
+    testOperation(
+        ctx.makeRectangle(180, 180, -10, 10),
+        SpatialOperation.IsWithin,
+        ctx.makeRectangle(-180, -100, -10, 10), true);
+  }
+
+  @Test
+  public void testContainsBugDatelineEdge() throws IOException {
+    setupGeo();
+    testOperation(
+        ctx.makeRectangle(-180, -150, -10, 10),
+        SpatialOperation.Contains,
+        ctx.makeRectangle(180, 180, -10, 10), true);
+  }
+
+  @Test
+  public void testWorldContainsXDL() throws IOException {
+    setupGeo();
+    testOperation(
+        ctx.makeRectangle(-180, 180, -10, 10),
+        SpatialOperation.Contains,
+        ctx.makeRectangle(170, -170, -10, 10), true);
+  }
+
+  /** See https://github.com/spatial4j/spatial4j/issues/85 */
+  @Test
+  public void testAlongDatelineOppositeSign() throws IOException {
+    // Due to Spatial4j bug #85, we can't simply do:
+    //    testOperation(indexedShape,
+    //        SpatialOperation.IsWithin,
+    //        queryShape, true);
+
+    //both on dateline but expressed using opposite signs
+    setupGeo();
+    final Rectangle indexedShape = ctx.makeRectangle(180, 180, -10, 10);
+    final Rectangle queryShape = ctx.makeRectangle(-180, -180, -20, 20);
+    final SpatialOperation operation = SpatialOperation.IsWithin;
+    final boolean match = true;//yes it is within
+
+    //the rest is super.testOperation without leading assert:
+
+    adoc("0", indexedShape);
+    commit();
+    Query query = strategy.makeQuery(new SpatialArgs(operation, queryShape));
+    SearchResults got = executeQuery(query, 1);
+    assert got.numFound <= 1 : "unclean test env";
+    if ((got.numFound == 1) != match)
+      fail(operation+" I:" + indexedShape + " Q:" + queryShape);
+    deleteAll();//clean up after ourselves
+  }
+
+  private void setupGeo() {
+    this.ctx = SpatialContext.GEO;
+    this.strategy = new BBoxStrategy(ctx, "bbox");
+  }
+
+  // OLD STATIC TESTS (worthless?)
+
+  @Test @Ignore("Overlaps not supported")
+  public void testBasicOperaions() throws IOException {
+    setupGeo();
+    getAddAndVerifyIndexedDocuments(DATA_SIMPLE_BBOX);
+
+    executeQueries(SpatialMatchConcern.EXACT, QTEST_Simple_Queries_BBox);
+  }
+
+  @Test
+  public void testStatesBBox() throws IOException {
+    setupGeo();
+    getAddAndVerifyIndexedDocuments(DATA_STATES_BBOX);
+
+    executeQueries(SpatialMatchConcern.FILTER, QTEST_States_IsWithin_BBox);
+    executeQueries(SpatialMatchConcern.FILTER, QTEST_States_Intersects_BBox);
+  }
+
+  @Test
+  public void testCitiesIntersectsBBox() throws IOException {
+    setupGeo();
+    getAddAndVerifyIndexedDocuments(DATA_WORLD_CITIES_POINTS);
+
+    executeQueries(SpatialMatchConcern.FILTER, QTEST_Cities_Intersects_BBox);
+  }
+
+  /* Convert DATA_WORLD_CITIES_POINTS to bbox */
+  @Override
+  protected Shape convertShapeFromGetDocuments(Shape shape) {
+    return shape.getBoundingBox();
+  }
+
+  public void testOverlapRatio() throws IOException {
+    setupGeo();
+
+    //Simply assert null shape results in 0
+    adoc("999", (Shape) null);
+    commit();
+    BBoxStrategy bboxStrategy = (BBoxStrategy) strategy;
+    checkValueSource(bboxStrategy.makeOverlapRatioValueSource(randomRectangle(), 0.0), new float[]{0f}, 0f);
+
+    //we test raw BBoxOverlapRatioValueSource without actual indexing
+    for (int SHIFT = 0; SHIFT < 360; SHIFT += 10) {
+      Rectangle queryBox = shiftedRect(0, 40, -20, 20, SHIFT);//40x40, 1600 area
+
+      final boolean MSL = random().nextBoolean();
+      final double minSideLength = MSL ? 0.1 : 0.0;
+      BBoxOverlapRatioValueSource sim = new BBoxOverlapRatioValueSource(null, true, queryBox, 0.5, minSideLength);
+      int nudge = SHIFT == 0 ? 0 : random().nextInt(3) * 10 - 10;//-10, 0, or 10.  Keep 0 on first round.
+
+      final double EPS = 0.0000001;
+
+      assertEquals("within", (200d/1600d * 0.5) + (0.5), sim.score(shiftedRect(10, 30, 0, 10, SHIFT + nudge), null), EPS);
+
+      assertEquals("in25%", 0.25, sim.score(shiftedRect(30, 70, -20, 20, SHIFT), null), EPS);
+
+      assertEquals("wrap", 0.2794117, sim.score(shiftedRect(30, 10, -20, 20, SHIFT + nudge), null), EPS);
+
+      assertEquals("no intersection H", 0.0, sim.score(shiftedRect(-10, -10, -20, 20, SHIFT), null), EPS);
+      assertEquals("no intersection V", 0.0, sim.score(shiftedRect(0, 20, -30, -30, SHIFT), null), EPS);
+
+      assertEquals("point", 0.5 + (MSL?(0.1*0.1/1600.0/2.0):0), sim.score(shiftedRect(0, 0, 0, 0, SHIFT), null), EPS);
+
+      assertEquals("line 25% intersection", 0.25/2 + (MSL?(10.0*0.1/1600.0/2.0):0.0), sim.score(shiftedRect(-30, 10, 0, 0, SHIFT), null), EPS);
+
+      //test with point query
+      sim = new BBoxOverlapRatioValueSource(null, true, shiftedRect(0, 0, 0, 0, SHIFT), 0.5, minSideLength);
+      assertEquals("same", 1.0, sim.score(shiftedRect(0, 0, 0, 0, SHIFT), null), EPS);
+      assertEquals("contains", 0.5 + (MSL?(0.1*0.1/(30*10)/2.0):0.0), sim.score(shiftedRect(0, 30, 0, 10, SHIFT), null), EPS);
+
+      //test with line query (vertical this time)
+      sim = new BBoxOverlapRatioValueSource(null, true, shiftedRect(0, 0, 20, 40, SHIFT), 0.5, minSideLength);
+      assertEquals("line 50%", 0.5, sim.score(shiftedRect(0, 0, 10, 30, SHIFT), null), EPS);
+      assertEquals("point", 0.5 + (MSL?(0.1*0.1/(20*0.1)/2.0):0.0), sim.score(shiftedRect(0, 0, 30, 30, SHIFT), null), EPS);
+    }
+
+  }
+
+  private Rectangle shiftedRect(double minX, double maxX, double minY, double maxY, int xShift) {
+    return ctx.makeRectangle(
+        DistanceUtils.normLonDEG(minX + xShift),
+        DistanceUtils.normLonDEG(maxX + xShift),
+        minY, maxY);
+  }
+
+  public void testAreaValueSource() throws IOException {
+    setupGeo();
+    //test we can disable indexed for this test
+    BBoxStrategy bboxStrategy = (BBoxStrategy) strategy;
+    if (random().nextBoolean()) {
+      FieldType fieldType = new FieldType(bboxStrategy.getFieldType());
+      fieldType.setIndexOptions(IndexOptions.NONE);
+      bboxStrategy.setFieldType(fieldType);
+    }
+
+    adoc("100", ctx.makeRectangle(0, 20, 40, 80));
+    adoc("999", (Shape) null);
+    commit();
+    checkValueSource(new ShapeAreaValueSource(bboxStrategy.makeShapeValueSource(), ctx, false, 1.0),
+        new float[]{800f, 0f}, 0f);
+    checkValueSource(new ShapeAreaValueSource(bboxStrategy.makeShapeValueSource(), ctx, true, 1.0),//geo
+        new float[]{391.93f, 0f}, 0.01f);
+    checkValueSource(new ShapeAreaValueSource(bboxStrategy.makeShapeValueSource(), ctx, true, 2.0),
+        new float[]{783.86f, 0f}, 0.01f); // testing with a different multiplier
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/composite/CompositeStrategyTest.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/composite/CompositeStrategyTest.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/composite/CompositeStrategyTest.java
new file mode 100644
index 0000000..8e1bb51
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/composite/CompositeStrategyTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.spatial.composite;
+
+import java.io.IOException;
+
+import com.carrotsearch.randomizedtesting.annotations.Repeat;
+import com.spatial4j.core.context.SpatialContext;
+import com.spatial4j.core.context.SpatialContextFactory;
+import com.spatial4j.core.shape.Point;
+import com.spatial4j.core.shape.Rectangle;
+import com.spatial4j.core.shape.Shape;
+import com.spatial4j.core.shape.impl.RectangleImpl;
+import org.apache.lucene.spatial.prefix.RandomSpatialOpStrategyTestCase;
+import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
+import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
+import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
+import org.apache.lucene.spatial.query.SpatialOperation;
+import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
+import org.junit.Test;
+
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomBoolean;
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomDouble;
+import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
+
+public class CompositeStrategyTest extends RandomSpatialOpStrategyTestCase {
+
+  private SpatialPrefixTree grid;
+  private RecursivePrefixTreeStrategy rptStrategy;
+
+  private void setupQuadGrid(int maxLevels) {
+    //non-geospatial makes this test a little easier (in gridSnap), and using boundary values 2^X raises
+    // the prospect of edge conditions we want to test, plus makes for simpler numbers (no decimals).
+    SpatialContextFactory factory = new SpatialContextFactory();
+    factory.geo = false;
+    factory.worldBounds = new RectangleImpl(0, 256, -128, 128, null);
+    this.ctx = factory.newSpatialContext();
+    //A fairly shallow grid
+    if (maxLevels == -1)
+      maxLevels = randomIntBetween(1, 8);//max 64k cells (4^8), also 256*256
+    this.grid = new QuadPrefixTree(ctx, maxLevels);
+    this.rptStrategy = newRPT();
+  }
+
+  private void setupGeohashGrid(int maxLevels) {
+    this.ctx = SpatialContext.GEO;
+    //A fairly shallow grid
+    if (maxLevels == -1)
+      maxLevels = randomIntBetween(1, 3);//max 16k cells (32^3)
+    this.grid = new GeohashPrefixTree(ctx, maxLevels);
+    this.rptStrategy = newRPT();
+  }
+
+  protected RecursivePrefixTreeStrategy newRPT() {
+    final RecursivePrefixTreeStrategy rpt = new RecursivePrefixTreeStrategy(this.grid,
+        getClass().getSimpleName() + "_rpt");
+    rpt.setDistErrPct(0.10);//not too many cells
+    return rpt;
+  }
+
+  @Test
+  @Repeat(iterations = 20)
+  public void testOperations() throws IOException {
+    //setup
+    if (randomBoolean()) {
+      setupQuadGrid(-1);
+    } else {
+      setupGeohashGrid(-1);
+    }
+    SerializedDVStrategy serializedDVStrategy = new SerializedDVStrategy(ctx, getClass().getSimpleName() + "_sdv");
+    this.strategy = new CompositeSpatialStrategy("composite_" + getClass().getSimpleName(),
+        rptStrategy, serializedDVStrategy);
+
+    //Do it!
+
+    for (SpatialOperation pred : SpatialOperation.values()) {
+      if (pred == SpatialOperation.BBoxIntersects || pred == SpatialOperation.BBoxWithin) {
+        continue;
+      }
+      if (pred == SpatialOperation.IsDisjointTo) {//TODO
+        continue;
+      }
+      testOperationRandomShapes(pred);
+      deleteAll();
+      commit();
+    }
+  }
+
+  @Override
+  protected boolean needsDocValues() {
+    return true;//due to SerializedDVStrategy
+  }
+
+  @Override
+  protected Shape randomIndexedShape() {
+    return randomShape();
+  }
+
+  @Override
+  protected Shape randomQueryShape() {
+    return randomShape();
+  }
+
+  private Shape randomShape() {
+    return random().nextBoolean() ? randomCircle() : randomRectangle();
+  }
+
+  //TODO move up
+  private Shape randomCircle() {
+    final Point point = randomPoint();
+    //TODO pick using gaussian
+    double radius;
+    if (ctx.isGeo()) {
+      radius = randomDouble() * 100;
+    } else {
+      //find distance to closest edge
+      final Rectangle worldBounds = ctx.getWorldBounds();
+      double maxRad = point.getX() - worldBounds.getMinX();
+      maxRad = Math.min(maxRad, worldBounds.getMaxX() - point.getX());
+      maxRad = Math.min(maxRad, point.getY() - worldBounds.getMinY());
+      maxRad = Math.min(maxRad, worldBounds.getMaxY() - point.getY());
+      radius = randomDouble() * maxRad;
+    }
+
+    return ctx.makeCircle(point, radius);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/50a2f754/lucene/spatial-extras/src/test/org/apache/lucene/spatial/prefix/CellToBytesRefIterator50.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/prefix/CellToBytesRefIterator50.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/prefix/CellToBytesRefIterator50.java
new file mode 100644
index 0000000..cccec8e
--- /dev/null
+++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/prefix/CellToBytesRefIterator50.java
@@ -0,0 +1,44 @@
+/*
+ * 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.spatial.prefix;
+
+import org.apache.lucene.spatial.prefix.tree.Cell;
+import org.apache.lucene.util.BytesRef;
+
+/** For testing Lucene <= 5.0. Index redundant prefixes for leaf cells. Fixed in LUCENE-4942. */
+class CellToBytesRefIterator50 extends CellToBytesRefIterator {
+
+  Cell repeatCell;
+
+  @Override
+  public BytesRef next() {
+    if (repeatCell != null) {
+      bytesRef = repeatCell.getTokenBytesWithLeaf(bytesRef);
+      repeatCell = null;
+      return bytesRef;
+    }
+    if (!cellIter.hasNext()) {
+      return null;
+    }
+    Cell cell = cellIter.next();
+    bytesRef = cell.getTokenBytesNoLeaf(bytesRef);
+    if (cell.isLeaf()) {
+      repeatCell = cell;
+    }
+    return bytesRef;
+  }
+}