| using Lucene.Net.Documents; |
| using Lucene.Net.Search; |
| using Lucene.Net.Spatial.Prefix.Tree; |
| using Lucene.Net.Spatial.Queries; |
| using Lucene.Net.Support; |
| using NUnit.Framework; |
| using Spatial4n.Core.Context; |
| using Spatial4n.Core.Shapes; |
| using Spatial4n.Core.Shapes.Impl; |
| using System; |
| using System.Collections.Generic; |
| using System.Linq; |
| using JCG = J2N.Collections.Generic; |
| using Console = Lucene.Net.Util.SystemConsole; |
| |
| namespace Lucene.Net.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. |
| */ |
| |
| public class SpatialOpRecursivePrefixTreeTest : StrategyTestCase |
| { |
| const int ITERATIONS = 1;//Test Iterations |
| |
| private SpatialPrefixTree grid; |
| |
| |
| public override void SetUp() |
| { |
| base.SetUp(); |
| DeleteAll(); |
| } |
| |
| public virtual void SetupGrid(int maxLevels) |
| { |
| if (Random.nextBoolean()) |
| SetupQuadGrid(maxLevels); |
| else |
| SetupGeohashGrid(maxLevels); |
| //((PrefixTreeStrategy) strategy).setDistErrPct(0);//fully precise to grid |
| |
| Console.WriteLine("Strategy: " + strategy.toString()); |
| } |
| |
| 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). |
| FakeSpatialContextFactory factory = new FakeSpatialContextFactory(); |
| factory.geo = false; |
| factory.worldBounds = new Rectangle(0, 256, -128, 128, null); |
| this.ctx = factory.NewSpatialContext(); |
| //A fairly shallow grid, and default 2.5% distErrPct |
| if (maxLevels == -1) |
| maxLevels = randomIntBetween(1, 8);//max 64k cells (4^8), also 256*256 |
| this.grid = new QuadPrefixTree(ctx, maxLevels); |
| this.strategy = new RecursivePrefixTreeStrategy(grid, GetType().Name); |
| } |
| |
| /// <summary> |
| /// LUCENENET specific class used to gain access to protected internal |
| /// member NewSpatialContext(), since we are not strong-named and |
| /// InternalsVisibleTo is not an option from a strong-named class. |
| /// </summary> |
| private class FakeSpatialContextFactory : SpatialContextFactory |
| { |
| new public SpatialContext NewSpatialContext() |
| { |
| return base.NewSpatialContext(); |
| } |
| } |
| |
| public virtual void SetupGeohashGrid(int maxLevels) |
| { |
| this.ctx = SpatialContext.GEO; |
| //A fairly shallow grid, and default 2.5% distErrPct |
| if (maxLevels == -1) |
| maxLevels = randomIntBetween(1, 3);//max 16k cells (32^3) |
| this.grid = new GeohashPrefixTree(ctx, maxLevels); |
| this.strategy = new RecursivePrefixTreeStrategy(grid, GetType().Name); |
| } |
| |
| [Test, Repeat(ITERATIONS)] |
| public virtual void TestIntersects() |
| { |
| SetupGrid(-1); |
| doTest(SpatialOperation.Intersects); |
| } |
| |
| [Test, Repeat(ITERATIONS)] |
| public virtual void TestWithin() |
| { |
| SetupGrid(-1); |
| doTest(SpatialOperation.IsWithin); |
| } |
| |
| [Test, Repeat(ITERATIONS)] |
| public virtual void TestContains() |
| { |
| SetupGrid(-1); |
| doTest(SpatialOperation.Contains); |
| } |
| |
| [Test, Repeat(ITERATIONS)] |
| public virtual void TestDisjoint() |
| { |
| SetupGrid(-1); |
| doTest(SpatialOperation.IsDisjointTo); |
| } |
| |
| /** See LUCENE-5062, <see cref="ContainsPrefixTreeFilter.m_multiOverlappingIndexedShapes"/>. */ |
| [Test, Repeat(ITERATIONS)] |
| public virtual void TestContainsPairOverlap() |
| { |
| SetupQuadGrid(3); |
| adoc("0", new ShapePair(ctx.MakeRectangle(0, 33, -128, 128), ctx.MakeRectangle(33, 128, -128, 128), true, ctx)); |
| Commit(); |
| Query query = strategy.MakeQuery(new SpatialArgs(SpatialOperation.Contains, |
| ctx.MakeRectangle(0, 128, -16, 128))); |
| SearchResults searchResults = executeQuery(query, 1); |
| assertEquals(1, searchResults.numFound); |
| } |
| |
| [Test] |
| public virtual void TestWithinDisjointParts() |
| { |
| SetupQuadGrid(7); |
| //one shape comprised of two parts, quite separated apart |
| adoc("0", new ShapePair(ctx.MakeRectangle(0, 10, -120, -100), ctx.MakeRectangle(220, 240, 110, 125), false, ctx)); |
| Commit(); |
| //query surrounds only the second part of the indexed shape |
| Query query = strategy.MakeQuery(new SpatialArgs(SpatialOperation.IsWithin, |
| ctx.MakeRectangle(210, 245, 105, 128))); |
| SearchResults searchResults = executeQuery(query, 1); |
| //we shouldn't find it because it's not completely within |
| assertTrue(searchResults.numFound == 0); |
| } |
| |
| [Test] /** LUCENE-4916 */ |
| public virtual void TestWithinLeafApproxRule() |
| { |
| SetupQuadGrid(2);//4x4 grid |
| //indexed shape will simplify to entire right half (2 top cells) |
| adoc("0", ctx.MakeRectangle(192, 204, -128, 128)); |
| Commit(); |
| |
| ((RecursivePrefixTreeStrategy)strategy).PrefixGridScanLevel = (Random.nextInt(2 + 1)); |
| |
| //query does NOT contain it; both indexed cells are leaves to the query, and |
| // when expanded to the full grid cells, the top one's top row is disjoint |
| // from the query and thus not a match. |
| assertTrue(executeQuery(strategy.MakeQuery( |
| new SpatialArgs(SpatialOperation.IsWithin, ctx.MakeRectangle(38, 192, -72, 56)) |
| ), 1).numFound == 0);//no-match |
| |
| //this time the rect is a little bigger and is considered a match. It's a |
| // an acceptable false-positive because of the grid approximation. |
| assertTrue(executeQuery(strategy.MakeQuery( |
| new SpatialArgs(SpatialOperation.IsWithin, ctx.MakeRectangle(38, 192, -72, 80)) |
| ), 1).numFound == 1);//match |
| } |
| |
| //Override so we can index parts of a pair separately, resulting in the detailLevel |
| // being independent for each shape vs the whole thing |
| protected override Document newDoc(String id, IShape shape) |
| { |
| Document doc = new Document(); |
| doc.Add(new StringField("id", id, Field.Store.YES)); |
| if (shape != null) |
| { |
| IList<IShape> shapes; |
| if (shape is ShapePair) |
| { |
| shapes = new List<IShape>(2); |
| shapes.Add(((ShapePair)shape).shape1); |
| shapes.Add(((ShapePair)shape).shape2); |
| } |
| else |
| { |
| shapes = new List<IShape>(new IShape[] { shape });//Collections.Singleton(shape); |
| } |
| foreach (IShape shapei in shapes) |
| { |
| foreach (Field f in strategy.CreateIndexableFields(shapei)) |
| { |
| doc.Add(f); |
| } |
| } |
| if (storeShape)//just for diagnostics |
| doc.Add(new StoredField(strategy.FieldName, shape.toString())); |
| } |
| return doc; |
| } |
| |
| private void doTest(SpatialOperation operation) |
| { |
| //first show that when there's no data, a query will result in no results |
| { |
| Query query = strategy.MakeQuery(new SpatialArgs(operation, randomRectangle())); |
| SearchResults searchResults = executeQuery(query, 1); |
| assertEquals(0, searchResults.numFound); |
| } |
| |
| bool biasContains = (operation == SpatialOperation.Contains); |
| |
| //Main index loop: |
| IDictionary<String, IShape> indexedShapes = new JCG.LinkedDictionary<String, IShape>(); |
| IDictionary<String, IShape> indexedShapesGS = new JCG.LinkedDictionary<String, IShape>();//grid snapped |
| int numIndexedShapes = randomIntBetween(1, 6); |
| #pragma warning disable 219 |
| bool indexedAtLeastOneShapePair = false; |
| #pragma warning restore 219 |
| for (int i = 0; i < numIndexedShapes; i++) |
| { |
| String id = "" + i; |
| IShape indexedShape; |
| int R = Random.nextInt(12); |
| if (R == 0) |
| {//1 in 12 |
| indexedShape = null; |
| } |
| else if (R == 1) |
| {//1 in 12 |
| indexedShape = randomPoint();//just one point |
| } |
| else if (R <= 4) |
| {//3 in 12 |
| //comprised of more than one shape |
| indexedShape = randomShapePairRect(biasContains); |
| indexedAtLeastOneShapePair = true; |
| } |
| else |
| { |
| indexedShape = randomRectangle();//just one rect |
| } |
| |
| indexedShapes.Put(id, indexedShape); |
| indexedShapesGS.Put(id, gridSnap(indexedShape)); |
| |
| adoc(id, indexedShape); |
| |
| if (Random.nextInt(10) == 0) |
| Commit();//intermediate commit, produces extra segments |
| |
| } |
| //delete some documents randomly |
| IEnumerator<String> idIter = indexedShapes.Keys.ToList().GetEnumerator(); |
| while (idIter.MoveNext()) |
| { |
| String id = idIter.Current; |
| if (Random.nextInt(10) == 0) |
| { |
| DeleteDoc(id); |
| //idIter.Remove(); |
| indexedShapes.Remove(id); |
| indexedShapesGS.Remove(id); |
| } |
| } |
| |
| Commit(); |
| |
| //Main query loop: |
| int numQueryShapes = AtLeast(20); |
| for (int i = 0; i < numQueryShapes; i++) |
| { |
| int scanLevel = randomInt(grid.MaxLevels); |
| ((RecursivePrefixTreeStrategy)strategy).PrefixGridScanLevel = (scanLevel); |
| |
| IShape queryShape; |
| switch (randomInt(10)) |
| { |
| case 0: queryShape = randomPoint(); break; |
| // LUCENE-5549 |
| //TODO debug: -Dtests.method=testWithin -Dtests.multiplier=3 -Dtests.seed=5F5294CE2E075A3E:AAD2F0F79288CA64 |
| // case 1:case 2:case 3: |
| // if (!indexedAtLeastOneShapePair) { // avoids ShapePair.relate(ShapePair), which isn't reliable |
| // queryShape = randomShapePairRect(!biasContains);//invert biasContains for query side |
| // break; |
| // } |
| default: queryShape = randomRectangle(); break; |
| } |
| IShape queryShapeGS = gridSnap(queryShape); |
| |
| bool opIsDisjoint = operation == SpatialOperation.IsDisjointTo; |
| |
| //Generate truth via brute force: |
| // We ensure true-positive matches (if the predicate on the raw shapes match |
| // then the search should find those same matches). |
| // approximations, false-positive matches |
| ISet<string> expectedIds = new JCG.LinkedHashSet<string>();//true-positives |
| ISet<string> secondaryIds = new JCG.LinkedHashSet<string>();//false-positives (unless disjoint) |
| foreach (var entry in indexedShapes) |
| { |
| string id = entry.Key; |
| IShape indexedShapeCompare = entry.Value; |
| if (indexedShapeCompare == null) |
| continue; |
| IShape queryShapeCompare = queryShape; |
| |
| if (operation.Evaluate(indexedShapeCompare, queryShapeCompare)) |
| { |
| expectedIds.Add(id); |
| if (opIsDisjoint) |
| { |
| //if no longer intersect after buffering them, for disjoint, remember this |
| indexedShapeCompare = indexedShapesGS[id]; |
| queryShapeCompare = queryShapeGS; |
| if (!operation.Evaluate(indexedShapeCompare, queryShapeCompare)) |
| secondaryIds.Add(id); |
| } |
| } |
| else if (!opIsDisjoint) |
| { |
| //buffer either the indexed or query shape (via gridSnap) and try again |
| if (operation == SpatialOperation.Intersects) |
| { |
| indexedShapeCompare = indexedShapesGS[id]; |
| queryShapeCompare = queryShapeGS; |
| //TODO Unfortunately, grid-snapping both can result in intersections that otherwise |
| // wouldn't happen when the grids are adjacent. Not a big deal but our test is just a |
| // bit more lenient. |
| } |
| else if (operation == SpatialOperation.Contains) |
| { |
| indexedShapeCompare = indexedShapesGS[id]; |
| } |
| else if (operation == SpatialOperation.IsWithin) |
| { |
| queryShapeCompare = queryShapeGS; |
| } |
| if (operation.Evaluate(indexedShapeCompare, queryShapeCompare)) |
| secondaryIds.Add(id); |
| } |
| } |
| |
| //Search and verify results |
| SpatialArgs args = new SpatialArgs(operation, queryShape); |
| if (queryShape is ShapePair) |
| args.DistErrPct = (0.0);//a hack; we want to be more detailed than gridSnap(queryShape) |
| Query query = strategy.MakeQuery(args); |
| SearchResults got = executeQuery(query, 100); |
| ISet<String> remainingExpectedIds = new JCG.LinkedHashSet<string>(expectedIds); |
| foreach (SearchResult result in got.results) |
| { |
| String id = result.GetId(); |
| bool removed = remainingExpectedIds.Remove(id); |
| if (!removed && (!opIsDisjoint && !secondaryIds.Contains(id))) |
| { |
| fail("Shouldn't match", id, indexedShapes, indexedShapesGS, queryShape); |
| } |
| } |
| if (opIsDisjoint) |
| remainingExpectedIds.ExceptWith(secondaryIds); |
| if (remainingExpectedIds.Count > 0) |
| { |
| var iter = remainingExpectedIds.GetEnumerator(); |
| iter.MoveNext(); |
| String id = iter.Current; |
| fail("Should have matched", id, indexedShapes, indexedShapesGS, queryShape); |
| } |
| } |
| } |
| |
| private IShape randomShapePairRect(bool biasContains) |
| { |
| IRectangle shape1 = randomRectangle(); |
| IRectangle shape2 = randomRectangle(); |
| return new ShapePair(shape1, shape2, biasContains, ctx); |
| } |
| |
| private void fail(String label, String id, IDictionary<String, IShape> indexedShapes, IDictionary<String, IShape> indexedShapesGS, IShape queryShape) |
| { |
| Console.WriteLine("Ig:" + indexedShapesGS[id] + " Qg:" + gridSnap(queryShape)); |
| fail(label + " I#" + id + ":" + indexedShapes[id] + " Q:" + queryShape); |
| } |
| |
| // private Rectangle inset(Rectangle r) { |
| // //typically inset by 1 (whole numbers are easy to read) |
| // double d = Math.min(1.0, grid.getDistanceForLevel(grid.getMaxLevels()) / 4); |
| // return ctx.makeRectangle(r.getMinX() + d, r.getMaxX() - d, r.getMinY() + d, r.getMaxY() - d); |
| // } |
| |
| protected IShape gridSnap(IShape snapMe) |
| { |
| if (snapMe == null) |
| return null; |
| if (snapMe is ShapePair) |
| { |
| ShapePair me = (ShapePair)snapMe; |
| return new ShapePair(gridSnap(me.shape1), gridSnap(me.shape2), me.biasContainsThenWithin, ctx); |
| } |
| if (snapMe is IPoint) |
| { |
| snapMe = snapMe.BoundingBox; |
| } |
| //The next 4 lines mimic PrefixTreeStrategy.createIndexableFields() |
| double distErrPct = ((PrefixTreeStrategy)strategy).DistErrPct; |
| double distErr = SpatialArgs.CalcDistanceFromErrPct(snapMe, distErrPct, ctx); |
| int detailLevel = grid.GetLevelForDistance(distErr); |
| IList<Cell> cells = grid.GetCells(snapMe, detailLevel, false, true); |
| |
| //calc bounding box of cells. |
| List<IShape> cellShapes = new List<IShape>(cells.size()); |
| foreach (Cell cell in cells) |
| { |
| cellShapes.Add(cell.Shape); |
| } |
| return new ShapeCollection(cellShapes, ctx).BoundingBox; |
| } |
| |
| /** |
| * An aggregate of 2 shapes. Unfortunately we can't simply use a ShapeCollection because: |
| * (a) ambiguity between CONTAINS & WITHIN for equal shapes, and |
| * (b) adjacent pairs could as a whole contain the input shape. |
| * The tests here are sensitive to these matters, although in practice ShapeCollection |
| * is fine. |
| */ |
| private class ShapePair : ShapeCollection /*<Shape>*/ |
| { |
| |
| private readonly SpatialContext ctx; |
| internal IShape shape1, shape2; |
| internal bool biasContainsThenWithin;//a hack |
| |
| public ShapePair(IShape shape1, IShape shape2, bool containsThenWithin, SpatialContext ctx) |
| : base(new List<IShape> { shape1, shape2 }, ctx) |
| { |
| this.ctx = ctx; |
| |
| this.shape1 = shape1; |
| this.shape2 = shape2; |
| biasContainsThenWithin = containsThenWithin; |
| } |
| |
| public override SpatialRelation Relate(IShape other) |
| { |
| SpatialRelation r = relateApprox(other); |
| if (r == SpatialRelation.CONTAINS) |
| return r; |
| if (r == SpatialRelation.DISJOINT) |
| return r; |
| if (r == SpatialRelation.WITHIN && !biasContainsThenWithin) |
| return r; |
| |
| //See if the correct answer is actually Contains, when the indexed shapes are adjacent, |
| // creating a larger shape that contains the input shape. |
| bool pairTouches = shape1.Relate(shape2).Intersects(); |
| if (!pairTouches) |
| return r; |
| //test all 4 corners |
| IRectangle oRect = (IRectangle)other; |
| if (Relate(ctx.MakePoint(oRect.MinX, oRect.MinY)) == SpatialRelation.CONTAINS |
| && Relate(ctx.MakePoint(oRect.MinX, oRect.MaxY)) == SpatialRelation.CONTAINS |
| && Relate(ctx.MakePoint(oRect.MaxX, oRect.MinY)) == SpatialRelation.CONTAINS |
| && Relate(ctx.MakePoint(oRect.MaxX, oRect.MaxY)) == SpatialRelation.CONTAINS) |
| return SpatialRelation.CONTAINS; |
| return r; |
| } |
| |
| private SpatialRelation relateApprox(IShape other) |
| { |
| if (biasContainsThenWithin) |
| { |
| if (shape1.Relate(other) == SpatialRelation.CONTAINS || shape1.equals(other) |
| || shape2.Relate(other) == SpatialRelation.CONTAINS || shape2.equals(other)) return SpatialRelation.CONTAINS; |
| |
| if (shape1.Relate(other) == SpatialRelation.WITHIN && shape2.Relate(other) == SpatialRelation.WITHIN) return SpatialRelation.WITHIN; |
| |
| } |
| else |
| { |
| if ((shape1.Relate(other) == SpatialRelation.WITHIN || shape1.equals(other)) |
| && (shape2.Relate(other) == SpatialRelation.WITHIN || shape2.equals(other))) return SpatialRelation.WITHIN; |
| |
| if (shape1.Relate(other) == SpatialRelation.CONTAINS || shape2.Relate(other) == SpatialRelation.CONTAINS) return SpatialRelation.CONTAINS; |
| } |
| |
| if (shape1.Relate(other).Intersects() || shape2.Relate(other).Intersects()) |
| return SpatialRelation.INTERSECTS;//might actually be 'CONTAINS' if the pair are adjacent but we handle that later |
| return SpatialRelation.DISJOINT; |
| } |
| |
| public override String ToString() |
| { |
| return "ShapePair(" + shape1 + " , " + shape2 + ")"; |
| } |
| } |
| } |
| } |