Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/SpatialPrefixTree.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/SpatialPrefixTree.java (revision 1457968) +++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/tree/SpatialPrefixTree.java (revision ) @@ -19,6 +19,7 @@ import com.spatial4j.core.context.SpatialContext; import com.spatial4j.core.shape.Point; +import com.spatial4j.core.shape.Rectangle; import com.spatial4j.core.shape.Shape; import java.nio.charset.Charset; @@ -77,7 +78,26 @@ */ public abstract int getLevelForDistance(double dist); - //TODO double getDistanceForLevel(int level) + /** + * Given a node having the specified level, returns the distance from opposite + * corners. Since this might very depending on where the node is, this method + * may over-estimate. + * + * @param level [1 to maxLevels] + * @return > 0 + */ + public double getDistanceForLevel(int level) { + if (level < 1 || level > getMaxLevels()) + throw new IllegalArgumentException("Level must be in 1 to maxLevels range"); + //TODO cache for each level + Node node = getNode(ctx.getWorldBounds().getCenter(), level); + Rectangle bbox = node.getShape().getBoundingBox(); + double width = bbox.getWidth(); + double height = bbox.getHeight(); + //Use standard cartesian hypotenuse. For geospatial, this answer is larger + // than the correct one but it's okay to over-estimate. + return Math.sqrt(width * width + height * height); + } private transient Node worldNode;//cached @@ -111,6 +131,9 @@ return target; } + /** + * Returns the cell containing point {@code p} at the specified {@code level}. + */ protected Node getNode(Point p, int level) { return getNodes(p, level, false).get(0); } Index: lucene/spatial/src/java/org/apache/lucene/spatial/query/SpatialOperation.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/java/org/apache/lucene/spatial/query/SpatialOperation.java (revision 1457968) +++ lucene/spatial/src/java/org/apache/lucene/spatial/query/SpatialOperation.java (revision ) @@ -17,6 +17,7 @@ * limitations under the License. */ +import com.spatial4j.core.shape.Rectangle; import com.spatial4j.core.shape.Shape; import com.spatial4j.core.shape.SpatialRelation; @@ -55,13 +56,14 @@ public static final SpatialOperation BBoxWithin = new SpatialOperation("BBoxWithin", true, false, false) { @Override public boolean evaluate(Shape indexedShape, Shape queryShape) { - return indexedShape.getBoundingBox().relate(queryShape) == SpatialRelation.WITHIN; + Rectangle bbox = indexedShape.getBoundingBox(); + return bbox.relate(queryShape) == SpatialRelation.WITHIN || bbox.equals(queryShape); } }; public static final SpatialOperation Contains = new SpatialOperation("Contains", true, true, false) { @Override public boolean evaluate(Shape indexedShape, Shape queryShape) { - return indexedShape.hasArea() && indexedShape.relate(queryShape) == SpatialRelation.CONTAINS; + return indexedShape.hasArea() && indexedShape.relate(queryShape) == SpatialRelation.CONTAINS || indexedShape.equals(queryShape); } }; public static final SpatialOperation Intersects = new SpatialOperation("Intersects", true, false, false) { @@ -85,7 +87,7 @@ public static final SpatialOperation IsWithin = new SpatialOperation("IsWithin", true, false, true) { @Override public boolean evaluate(Shape indexedShape, Shape queryShape) { - return queryShape.hasArea() && indexedShape.relate(queryShape) == SpatialRelation.WITHIN; + return queryShape.hasArea() && (indexedShape.relate(queryShape) == SpatialRelation.WITHIN || indexedShape.equals(queryShape)); } }; public static final SpatialOperation Overlaps = new SpatialOperation("Overlaps", true, false, true) { Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractVisitingPrefixTreeFilter.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractVisitingPrefixTreeFilter.java (revision 1457968) +++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractVisitingPrefixTreeFilter.java (revision ) @@ -90,7 +90,7 @@ * method then it's short-circuited until it finds one, at which point * {@link #visit(org.apache.lucene.spatial.prefix.tree.Node)} is called. At * some depths, of the tree, the algorithm switches to a scanning mode that - * finds calls {@link #visitScanned(org.apache.lucene.spatial.prefix.tree.Node, com.spatial4j.core.shape.Shape)} + * finds calls {@link #visitScanned(org.apache.lucene.spatial.prefix.tree.Node)} * for each leaf cell found. * * @lucene.internal @@ -207,7 +207,6 @@ throw new IllegalStateException("Spatial logic error"); //Check for adjacent leaf (happens for indexed non-point shapes) - assert !cell.isLeaf(); if (hasIndexedLeaves && cell.getLevel() != 0) { //If the next indexed term just adds a leaf marker ('+') to cell, // then add all of those docs @@ -257,8 +256,7 @@ * Scans ({@code termsEnum.next()}) terms until a term is found that does * not start with curVNode's cell. If it finds a leaf cell or a cell at * level {@code scanDetailLevel} then it calls {@link - * #visitScanned(org.apache.lucene.spatial.prefix.tree.Node, - * com.spatial4j.core.shape.Shape)}. + * #visitScanned(org.apache.lucene.spatial.prefix.tree.Node)}. */ protected void scan(int scanDetailLevel) throws IOException { for (; @@ -270,15 +268,7 @@ if (termLevel > scanDetailLevel) continue; if (termLevel == scanDetailLevel || scanCell.isLeaf()) { - Shape cShape; - //if this cell represents a point, use the cell center vs the box - // (points never have isLeaf()) - if (termLevel == grid.getMaxLevels() && !scanCell.isLeaf()) - cShape = scanCell.getCenter(); - else - cShape = scanCell.getShape(); - - visitScanned(scanCell, cShape); + visitScanned(scanCell); } }//term loop } @@ -337,10 +327,8 @@ /** * The cell is either indexed as a leaf or is the last level of detail. It * might not even intersect the query shape, so be sure to check for that. - * Use {@code cellShape} instead of {@code cell.getCellShape} for the cell's - * shape. */ - protected abstract void visitScanned(Node cell, Shape cellShape) throws IOException; + protected abstract void visitScanned(Node cell) throws IOException; protected void preSiblings(VNode vNode) throws IOException { Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java (revision 1457968) +++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java (revision ) @@ -62,17 +62,21 @@ @Override public Filter makeFilter(SpatialArgs args) { - final SpatialOperation op = args.getOperation(); - if (op != SpatialOperation.Intersects) - throw new UnsupportedSpatialOperation(op); Shape shape = args.getShape(); - int detailLevel = grid.getLevelForDistance(args.resolveDistErr(ctx, distErrPct)); + final boolean hasIndexedLeaves = true; + final SpatialOperation op = args.getOperation(); + if (op == SpatialOperation.Intersects) { - return new IntersectsPrefixTreeFilter( - shape, getFieldName(), grid, detailLevel, prefixGridScanLevel, + return new IntersectsPrefixTreeFilter( + shape, getFieldName(), grid, detailLevel, prefixGridScanLevel, - true);//hasIndexedLeaves + hasIndexedLeaves); + } else if (op == SpatialOperation.IsWithin) { + return new WithinPrefixTreeFilter( + shape, getFieldName(), grid, detailLevel, prefixGridScanLevel); + } + throw new UnsupportedSpatialOperation(op); } } Index: lucene/spatial/src/test/org/apache/lucene/spatial/prefix/SpatialOpRecursivePrefixTreeTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/test/org/apache/lucene/spatial/prefix/SpatialOpRecursivePrefixTreeTest.java (revision 1457968) +++ lucene/spatial/src/test/org/apache/lucene/spatial/prefix/SpatialOpRecursivePrefixTreeTest.java (revision ) @@ -29,9 +29,11 @@ import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree; import org.apache.lucene.spatial.query.SpatialArgs; import org.apache.lucene.spatial.query.SpatialOperation; +import org.junit.Before; import org.junit.Test; import java.io.IOException; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -45,10 +47,11 @@ private SpatialPrefixTree grid; - @Test - @Repeat(iterations = 20) - public void testIntersects() throws IOException { - //non-geospatial makes this test a little easier + @Before + public void setUp() throws Exception { + super.setUp(); + //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). this.ctx = new SpatialContext(false, null, new RectangleImpl(0, 256, -128, 128, null)); //A fairly shallow grid, and default 2.5% distErrPct this.grid = new QuadPrefixTree(ctx, randomIntBetween(1, 8)); @@ -56,56 +59,76 @@ //((PrefixTreeStrategy) strategy).setDistErrPct(0);//fully precise to grid deleteAll(); + System.out.println("Strategy: "+strategy.toString()); + } + @Test + @Repeat(iterations = 20) + public void testIntersects() throws IOException { + doTest(SpatialOperation.Intersects); + } + + @Test + @Repeat(iterations = 20) + public void testWithin() throws IOException { + doTest(SpatialOperation.IsWithin); + } + + private void doTest(final SpatialOperation operation) throws IOException { Map indexedShapes = new LinkedHashMap(); - Map indexedGriddedShapes = new LinkedHashMap(); final int numIndexedShapes = randomIntBetween(1, 6); for (int i = 1; i <= numIndexedShapes; i++) { String id = "" + i; - Shape indexShape = randomRectangle(); - Rectangle gridShape = gridSnapp(indexShape); - indexedShapes.put(id, indexShape); - indexedGriddedShapes.put(id, gridShape); - adoc(id, indexShape); + Rectangle indexedShape = gridSnapp(randomRectangle()); + indexedShapes.put(id, indexedShape); + adoc(id, indexedShape); + + if (random().nextInt(10) == 0) + commit(); } + //delete some + Iterator idIter = indexedShapes.keySet().iterator(); + while (idIter.hasNext()) { + String id = idIter.next(); + if (random().nextInt(10) == 0) { + deleteDoc(id); + idIter.remove(); + } + } + commit(); final int numQueryShapes = atLeast(10); for (int i = 0; i < numQueryShapes; i++) { int scanLevel = randomInt(grid.getMaxLevels()); ((RecursivePrefixTreeStrategy) strategy).setPrefixGridScanLevel(scanLevel); - Rectangle queryShape = randomRectangle(); - Rectangle queryGridShape = gridSnapp(queryShape); + Rectangle queryShape = gridSnapp(randomRectangle()); //Generate truth via brute force - final SpatialOperation operation = SpatialOperation.Intersects; Set expectedIds = new TreeSet(); - Set optionalIds = new TreeSet(); - for (String id : indexedShapes.keySet()) { - Shape indexShape = indexedShapes.get(id); - Rectangle indexGridShape = indexedGriddedShapes.get(id); - if (operation.evaluate(indexShape, queryShape)) - expectedIds.add(id); - else if (operation.evaluate(indexGridShape, queryGridShape)) - optionalIds.add(id); + for (Map.Entry entry : indexedShapes.entrySet()) { + if (operation.evaluate(entry.getValue(), queryShape)) + expectedIds.add(entry.getKey()); } //Search and verify results Query query = strategy.makeQuery(new SpatialArgs(operation, queryShape)); SearchResults got = executeQuery(query, 100); Set remainingExpectedIds = new TreeSet(expectedIds); - String msg = queryShape.toString()+" Expect: "+expectedIds+" Opt: "+optionalIds; + String msg = queryShape.toString()+" Expect: "+expectedIds; for (SearchResult result : got.results) { String id = result.getId(); Object removed = remainingExpectedIds.remove(id); if (removed == null) { - assertTrue("Shouldn't match " + id + " in "+msg, optionalIds.contains(id)); + fail("Shouldn't match " + id + " ("+ indexedShapes.get(id) +") in " + msg); } } - assertTrue("Didn't match " + remainingExpectedIds + " in " + msg, remainingExpectedIds.isEmpty()); + if (!remainingExpectedIds.isEmpty()) { + Shape firstFailedMatch = indexedShapes.get(remainingExpectedIds.iterator().next()); + fail("Didn't match " + firstFailedMatch + " in " + msg +" (of "+remainingExpectedIds.size()+")"); - } + } - + } } protected Rectangle gridSnapp(Shape snapMe) { Index: lucene/spatial/src/test/org/apache/lucene/spatial/StrategyTestCase.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/test/org/apache/lucene/spatial/StrategyTestCase.java (revision 1457968) +++ lucene/spatial/src/test/org/apache/lucene/spatial/StrategyTestCase.java (revision ) @@ -26,10 +26,12 @@ 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.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; @@ -201,6 +203,10 @@ doc.add(new StoredField(strategy.getFieldName(), ctx.toString(shape))); } return doc; + } + + protected void deleteDoc(String id) throws IOException { + indexWriter.deleteDocuments(new TermQuery(new Term("id", id))); } /** scores[] are in docId order */ Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/IntersectsPrefixTreeFilter.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/IntersectsPrefixTreeFilter.java (revision 1457968) +++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/IntersectsPrefixTreeFilter.java (revision ) @@ -24,7 +24,7 @@ import org.apache.lucene.spatial.prefix.tree.Node; import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree; import org.apache.lucene.util.Bits; -import org.apache.lucene.util.OpenBitSet; +import org.apache.lucene.util.FixedBitSet; import java.io.IOException; @@ -53,11 +53,11 @@ @Override public DocIdSet getDocIdSet(AtomicReaderContext context, Bits acceptDocs) throws IOException { return new VisitorTemplate(context, acceptDocs, hasIndexedLeaves) { - private OpenBitSet results; + private FixedBitSet results; @Override protected void start() { - results = new OpenBitSet(maxDoc); + results = new FixedBitSet(maxDoc); } @Override @@ -80,8 +80,16 @@ } @Override - protected void visitScanned(Node cell, Shape cellShape) throws IOException { - if (queryShape.relate(cellShape).intersects()) + protected void visitScanned(Node cell) throws IOException { + Shape cShape; + //if this cell represents a point, use the cell center vs the box + // TODO this behavior is debatable; might want to be configurable + // (points never have isLeaf()) + if (cell.getLevel() == grid.getMaxLevels() && !cell.isLeaf()) + cShape = cell.getCenter(); + else + cShape = cell.getShape(); + if (queryShape.relate(cShape).intersects()) collectDocs(results); } Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeFilter.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeFilter.java (revision 1457968) +++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeFilter.java (revision ) @@ -27,14 +27,14 @@ import org.apache.lucene.search.Filter; import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree; import org.apache.lucene.util.Bits; -import org.apache.lucene.util.OpenBitSet; +import org.apache.lucene.util.FixedBitSet; import java.io.IOException; /** * Base class for Lucene Filters on SpatialPrefixTree fields. * - * @lucene.internal + * @lucene.experimental */ public abstract class AbstractPrefixTreeFilter extends Filter { @@ -93,13 +93,13 @@ this.termsEnum = terms.iterator(null); } - protected void collectDocs(OpenBitSet bitSet) throws IOException { + protected void collectDocs(FixedBitSet bitSet) throws IOException { //WARN: keep this specialization in sync assert termsEnum != null; docsEnum = termsEnum.docs(acceptDocs, docsEnum, DocsEnum.FLAG_NONE); int docid; while ((docid = docsEnum.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { - bitSet.fastSet(docid); + bitSet.set(docid); } } Index: lucene/spatial/src/java/org/apache/lucene/spatial/prefix/WithinPrefixTreeFilter.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/java/org/apache/lucene/spatial/prefix/WithinPrefixTreeFilter.java (revision ) +++ lucene/spatial/src/java/org/apache/lucene/spatial/prefix/WithinPrefixTreeFilter.java (revision ) @@ -0,0 +1,164 @@ +package org.apache.lucene.spatial.prefix; + +/* + * 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. + */ + +import com.spatial4j.core.context.SpatialContext; +import com.spatial4j.core.distance.DistanceUtils; +import com.spatial4j.core.shape.Circle; +import com.spatial4j.core.shape.Point; +import com.spatial4j.core.shape.Rectangle; +import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.SpatialRelation; +import org.apache.lucene.index.AtomicReaderContext; +import org.apache.lucene.search.DocIdSet; +import org.apache.lucene.spatial.prefix.tree.Node; +import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.FixedBitSet; + +import java.io.IOException; +import java.util.Iterator; + +/** + * Finds docs where its indexed shape is {@link org.apache.lucene.spatial.query.SpatialOperation#IsWithin + * WITHIN} the query shape. If an indexed shape is comprised of multiple + * disjoint parts, then correct results depend on caching the center points of + * these shapes as well. + * + * @lucene.experimental + */ +public class WithinPrefixTreeFilter extends AbstractVisitingPrefixTreeFilter { + + private final Shape bufferedQueryShape; + + public WithinPrefixTreeFilter(Shape queryShape, String fieldName, SpatialPrefixTree grid, int detailLevel, int prefixGridScanLevel) { + super(queryShape, fieldName, grid, detailLevel, prefixGridScanLevel); + this.bufferedQueryShape = bufferQueryShape(queryShape); + } + + /** Returns a new query shape that is larger than queryShape by at least the + * size of the smallest grid cell. + */ + protected Shape bufferQueryShape(Shape queryShape) { + double distErr = grid.getDistanceForLevel(detailLevel); + SpatialContext ctx = grid.getSpatialContext(); + if (queryShape instanceof Point) { + return ctx.makeCircle((Point)queryShape, distErr); + } else if (queryShape instanceof Circle) { + Circle circle = (Circle) queryShape; + double newDist = circle.getRadius() + distErr; + if (ctx.isGeo() && newDist > 180) + newDist = 180; + return ctx.makeCircle(circle.getCenter(), newDist); + } else { + Rectangle bbox = queryShape.getBoundingBox(); + double newMinX = bbox.getMinX() - distErr; + double newMaxX = bbox.getMaxX() + distErr; + double newMinY = bbox.getMinY() - distErr; + double newMaxY = bbox.getMaxY() + distErr; + if (ctx.isGeo()) { + if (newMinY < -90) + newMinY = -90; + if (newMaxY > 90) + newMaxY = 90; + if (newMinY == -90 || newMaxY == 90 || bbox.getWidth() + 2*distErr > 360) { + newMinX = -180; + newMaxX = 180; + } else { + newMinX = DistanceUtils.normLonDEG(newMinX); + newMaxX = DistanceUtils.normLonDEG(newMaxX); + } + } else { + //restrict to world bounds + newMinX = Math.max(newMinX, ctx.getWorldBounds().getMinX()); + newMaxX = Math.min(newMaxX, ctx.getWorldBounds().getMaxX()); + newMinY = Math.max(newMinY, ctx.getWorldBounds().getMinY()); + newMaxY = Math.min(newMaxY, ctx.getWorldBounds().getMaxY()); + } + return ctx.makeRectangle(newMinX, newMaxX, newMinY, newMaxY); + } + } + + + @Override + public DocIdSet getDocIdSet(AtomicReaderContext context, Bits acceptDocs) throws IOException { + return new VisitorTemplate(context, acceptDocs, true) { + private FixedBitSet inside; + private FixedBitSet outside; + private SpatialRelation visitRelation; + + @Override + protected void start() { + inside = new FixedBitSet(maxDoc); + outside = new FixedBitSet(maxDoc); + } + + @Override + protected DocIdSet finish() { + inside.andNot(outside); + return inside; + } + + @Override + protected Iterator findSubCellsToVisit(Node cell) { + //use buffered query shape instead of orig + return cell.getSubCells(bufferedQueryShape).iterator(); + } + + @Override + protected boolean visit(Node cell) throws IOException { + //cell.relate is based on the bufferedQueryShape; we need to examine what + // the relation is against the queryShape + visitRelation = cell.getShape().relate(queryShape); + if (visitRelation == SpatialRelation.WITHIN) { + collectDocs(inside); + return false; + } else if (visitRelation == SpatialRelation.DISJOINT) { + collectDocs(outside); + return false; + } else if (cell.getLevel() == detailLevel) { + collectDocs(inside); + return false; + } + return true; + } + + @Override + protected void visitLeaf(Node cell) throws IOException { + SpatialRelation relation = visitRelation; + assert visitRelation == cell.getShape().relate(queryShape); + if (relation.intersects()) { + collectDocs(inside); + } else { + collectDocs(outside); + } + } + + @Override + protected void visitScanned(Node cell) throws IOException { + if (queryShape.relate(cell.getShape()).intersects()) { + collectDocs(inside); + } else { + collectDocs(outside); + } + } + + }.getDocIdSet(); + } + +} Index: lucene/spatial/src/test/org/apache/lucene/spatial/SpatialTestCase.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- lucene/spatial/src/test/org/apache/lucene/spatial/SpatialTestCase.java (revision 1457968) +++ lucene/spatial/src/test/org/apache/lucene/spatial/SpatialTestCase.java (revision ) @@ -45,7 +45,7 @@ public abstract class SpatialTestCase extends LuceneTestCase { private DirectoryReader indexReader; - private RandomIndexWriter indexWriter; + protected RandomIndexWriter indexWriter; private Directory directory; protected IndexSearcher indexSearcher;