| /* |
| * 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.spatial3d; |
| |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| import org.apache.lucene.codecs.Codec; |
| import org.apache.lucene.codecs.FilterCodec; |
| import org.apache.lucene.codecs.PointsFormat; |
| import org.apache.lucene.codecs.PointsReader; |
| import org.apache.lucene.codecs.PointsWriter; |
| import org.apache.lucene.codecs.lucene60.Lucene60PointsReader; |
| import org.apache.lucene.codecs.lucene60.Lucene60PointsWriter; |
| import org.apache.lucene.document.Document; |
| import org.apache.lucene.document.Field; |
| import org.apache.lucene.document.NumericDocValuesField; |
| import org.apache.lucene.spatial3d.geom.GeoArea; |
| import org.apache.lucene.spatial3d.geom.GeoAreaFactory; |
| import org.apache.lucene.spatial3d.geom.GeoBBoxFactory; |
| import org.apache.lucene.spatial3d.geom.GeoCircleFactory; |
| import org.apache.lucene.spatial3d.geom.GeoPath; |
| import org.apache.lucene.spatial3d.geom.GeoPoint; |
| import org.apache.lucene.spatial3d.geom.GeoPolygonFactory; |
| import org.apache.lucene.spatial3d.geom.GeoShape; |
| import org.apache.lucene.spatial3d.geom.PlanetModel; |
| import org.apache.lucene.spatial3d.geom.XYZBounds; |
| import org.apache.lucene.index.DirectoryReader; |
| import org.apache.lucene.index.IndexReader; |
| import org.apache.lucene.index.IndexWriter; |
| import org.apache.lucene.index.IndexWriterConfig; |
| import org.apache.lucene.index.LeafReaderContext; |
| import org.apache.lucene.index.MultiDocValues; |
| import org.apache.lucene.index.NumericDocValues; |
| import org.apache.lucene.index.SegmentReadState; |
| import org.apache.lucene.index.SegmentWriteState; |
| import org.apache.lucene.index.Term; |
| import org.apache.lucene.search.IndexSearcher; |
| import org.apache.lucene.search.Query; |
| import org.apache.lucene.search.SimpleCollector; |
| import org.apache.lucene.store.Directory; |
| import org.apache.lucene.util.FixedBitSet; |
| import org.apache.lucene.util.IOUtils; |
| import org.apache.lucene.util.LuceneTestCase; |
| import org.apache.lucene.util.TestUtil; |
| import org.junit.BeforeClass; |
| |
| import com.carrotsearch.randomizedtesting.generators.RandomInts; |
| |
| public class TestGeo3DPoint extends LuceneTestCase { |
| |
| private static boolean smallBBox; |
| |
| @BeforeClass |
| public static void beforeClass() { |
| smallBBox = random().nextBoolean(); |
| if (VERBOSE) { |
| System.err.println("TEST: smallBBox=" + smallBBox); |
| } |
| } |
| |
| private static Codec getCodec() { |
| if (Codec.getDefault().getName().equals("Lucene60")) { |
| int maxPointsInLeafNode = TestUtil.nextInt(random(), 16, 2048); |
| double maxMBSortInHeap = 3.0 + (3*random().nextDouble()); |
| if (VERBOSE) { |
| System.out.println("TEST: using Lucene60PointsFormat with maxPointsInLeafNode=" + maxPointsInLeafNode + " and maxMBSortInHeap=" + maxMBSortInHeap); |
| } |
| |
| return new FilterCodec("Lucene60", Codec.getDefault()) { |
| @Override |
| public PointsFormat pointsFormat() { |
| return new PointsFormat() { |
| @Override |
| public PointsWriter fieldsWriter(SegmentWriteState writeState) throws IOException { |
| return new Lucene60PointsWriter(writeState, maxPointsInLeafNode, maxMBSortInHeap); |
| } |
| |
| @Override |
| public PointsReader fieldsReader(SegmentReadState readState) throws IOException { |
| return new Lucene60PointsReader(readState); |
| } |
| }; |
| } |
| }; |
| } else { |
| return Codec.getDefault(); |
| } |
| } |
| |
| public void testBasic() throws Exception { |
| Directory dir = getDirectory(); |
| IndexWriterConfig iwc = newIndexWriterConfig(); |
| iwc.setCodec(getCodec()); |
| IndexWriter w = new IndexWriter(dir, iwc); |
| Document doc = new Document(); |
| doc.add(new Geo3DPoint("field", toRadians(50.7345267), toRadians(-97.5303555))); |
| w.addDocument(doc); |
| IndexReader r = DirectoryReader.open(w); |
| // We can't wrap with "exotic" readers because the query must see the BKD3DDVFormat: |
| IndexSearcher s = newSearcher(r, false); |
| assertEquals(1, s.search(Geo3DPoint.newShapeQuery("field", |
| GeoCircleFactory.makeGeoCircle(PlanetModel.WGS84, toRadians(50), toRadians(-97), Math.PI/180.)), 1).totalHits); |
| w.close(); |
| r.close(); |
| dir.close(); |
| } |
| |
| private static double toRadians(double degrees) { |
| return Math.PI*(degrees/360.0); |
| } |
| |
| private static PlanetModel getPlanetModel() { |
| if (random().nextBoolean()) { |
| // Use one of the earth models: |
| if (random().nextBoolean()) { |
| return PlanetModel.WGS84; |
| } else { |
| return PlanetModel.SPHERE; |
| } |
| } else { |
| // Make a randomly squashed planet: |
| double oblateness = random().nextDouble() * 0.5 - 0.25; |
| return new PlanetModel(1.0 + oblateness, 1.0 - oblateness); |
| } |
| } |
| |
| private static class Cell { |
| static int nextCellID; |
| |
| final Cell parent; |
| final int cellID; |
| final int xMinEnc, xMaxEnc; |
| final int yMinEnc, yMaxEnc; |
| final int zMinEnc, zMaxEnc; |
| final int splitCount; |
| |
| public Cell(Cell parent, |
| int xMinEnc, int xMaxEnc, |
| int yMinEnc, int yMaxEnc, |
| int zMinEnc, int zMaxEnc, |
| int splitCount) { |
| this.parent = parent; |
| this.xMinEnc = xMinEnc; |
| this.xMaxEnc = xMaxEnc; |
| this.yMinEnc = yMinEnc; |
| this.yMaxEnc = yMaxEnc; |
| this.zMinEnc = zMinEnc; |
| this.zMaxEnc = zMaxEnc; |
| this.cellID = nextCellID++; |
| this.splitCount = splitCount; |
| } |
| |
| /** Returns true if the quantized point lies within this cell, inclusive on all bounds. */ |
| public boolean contains(double planetMax, GeoPoint point) { |
| int docX = Geo3DUtil.encodeValue(planetMax, point.x); |
| int docY = Geo3DUtil.encodeValue(planetMax, point.y); |
| int docZ = Geo3DUtil.encodeValue(planetMax, point.z); |
| |
| return docX >= xMinEnc && docX <= xMaxEnc && |
| docY >= yMinEnc && docY <= yMaxEnc && |
| docZ >= zMinEnc && docZ <= zMaxEnc; |
| } |
| |
| @Override |
| public String toString() { |
| return "cell=" + cellID + (parent == null ? "" : " parentCellID=" + parent.cellID) + " x: " + xMinEnc + " TO " + xMaxEnc + ", y: " + yMinEnc + " TO " + yMaxEnc + ", z: " + zMinEnc + " TO " + zMaxEnc + ", splits: " + splitCount; |
| } |
| } |
| |
| private static GeoPoint quantize(double planetMax, GeoPoint point) { |
| return new GeoPoint(Geo3DUtil.decodeValueCenter(planetMax, Geo3DUtil.encodeValue(planetMax, point.x)), |
| Geo3DUtil.decodeValueCenter(planetMax, Geo3DUtil.encodeValue(planetMax, point.y)), |
| Geo3DUtil.decodeValueCenter(planetMax, Geo3DUtil.encodeValue(planetMax, point.z))); |
| } |
| |
| /** Tests consistency of GeoArea.getRelationship vs GeoShape.isWithin */ |
| public void testGeo3DRelations() throws Exception { |
| |
| PlanetModel planetModel = getPlanetModel(); |
| |
| int numDocs = atLeast(1000); |
| if (VERBOSE) { |
| System.out.println("TEST: " + numDocs + " docs"); |
| } |
| |
| GeoPoint[] docs = new GeoPoint[numDocs]; |
| for(int docID=0;docID<numDocs;docID++) { |
| docs[docID] = new GeoPoint(planetModel, toRadians(randomLat()), toRadians(randomLon())); |
| if (VERBOSE) { |
| System.out.println(" doc=" + docID + ": " + docs[docID]); |
| } |
| } |
| |
| double planetMax = planetModel.getMaximumMagnitude(); |
| |
| int iters = atLeast(10); |
| |
| int recurseDepth = RandomInts.randomIntBetween(random(), 5, 15); |
| |
| iters = atLeast(50); |
| |
| for(int iter=0;iter<iters;iter++) { |
| GeoShape shape = randomShape(planetModel); |
| |
| StringWriter sw = new StringWriter(); |
| PrintWriter log = new PrintWriter(sw, true); |
| |
| if (VERBOSE) { |
| log.println("TEST: iter=" + iter + " shape=" + shape); |
| } |
| |
| XYZBounds bounds = new XYZBounds(); |
| shape.getBounds(bounds); |
| |
| // Start with the root cell that fully contains the shape: |
| Cell root = new Cell(null, |
| Geo3DUtil.encodeValueLenient(planetMax, bounds.getMinimumX()), |
| Geo3DUtil.encodeValueLenient(planetMax, bounds.getMaximumX()), |
| Geo3DUtil.encodeValueLenient(planetMax, bounds.getMinimumY()), |
| Geo3DUtil.encodeValueLenient(planetMax, bounds.getMaximumY()), |
| Geo3DUtil.encodeValueLenient(planetMax, bounds.getMinimumZ()), |
| Geo3DUtil.encodeValueLenient(planetMax, bounds.getMaximumZ()), |
| 0); |
| |
| if (VERBOSE) { |
| log.println(" root cell: " + root); |
| } |
| |
| List<Cell> queue = new ArrayList<>(); |
| queue.add(root); |
| Set<Integer> hits = new HashSet<>(); |
| |
| while (queue.size() > 0) { |
| Cell cell = queue.get(queue.size()-1); |
| queue.remove(queue.size()-1); |
| if (VERBOSE) { |
| log.println(" cycle: " + cell + " queue.size()=" + queue.size()); |
| } |
| |
| if (random().nextInt(10) == 7 || cell.splitCount > recurseDepth) { |
| if (VERBOSE) { |
| log.println(" leaf"); |
| } |
| // Leaf cell: brute force check all docs that fall within this cell: |
| for(int docID=0;docID<numDocs;docID++) { |
| GeoPoint point = docs[docID]; |
| if (cell.contains(planetMax, point)) { |
| if (shape.isWithin(quantize(planetMax, point))) { |
| if (VERBOSE) { |
| log.println(" check doc=" + docID + ": match!"); |
| } |
| hits.add(docID); |
| } else { |
| if (VERBOSE) { |
| log.println(" check doc=" + docID + ": no match"); |
| } |
| } |
| } |
| } |
| } else { |
| |
| GeoArea xyzSolid = GeoAreaFactory.makeGeoArea(planetModel, |
| Geo3DUtil.decodeValueMin(planetMax, cell.xMinEnc), Geo3DUtil.decodeValueMax(planetMax, cell.xMaxEnc), |
| Geo3DUtil.decodeValueMin(planetMax, cell.yMinEnc), Geo3DUtil.decodeValueMax(planetMax, cell.yMaxEnc), |
| Geo3DUtil.decodeValueMin(planetMax, cell.zMinEnc), Geo3DUtil.decodeValueMax(planetMax, cell.zMaxEnc)); |
| |
| if (VERBOSE) { |
| log.println(" minx="+Geo3DUtil.decodeValueMin(planetMax, cell.xMinEnc)+" maxx="+Geo3DUtil.decodeValueMax(planetMax, cell.xMaxEnc)+ |
| " miny="+Geo3DUtil.decodeValueMin(planetMax, cell.yMinEnc)+" maxy="+Geo3DUtil.decodeValueMax(planetMax, cell.yMaxEnc)+ |
| " minz="+Geo3DUtil.decodeValueMin(planetMax, cell.zMinEnc)+" maxz="+Geo3DUtil.decodeValueMax(planetMax, cell.zMaxEnc)); |
| } |
| |
| switch (xyzSolid.getRelationship(shape)) { |
| case GeoArea.CONTAINS: |
| // Shape fully contains the cell: blindly add all docs in this cell: |
| if (VERBOSE) { |
| log.println(" GeoArea.CONTAINS: now addAll"); |
| } |
| for(int docID=0;docID<numDocs;docID++) { |
| if (cell.contains(planetMax, docs[docID])) { |
| if (VERBOSE) { |
| log.println(" addAll doc=" + docID); |
| } |
| hits.add(docID); |
| } |
| } |
| continue; |
| case GeoArea.OVERLAPS: |
| if (VERBOSE) { |
| log.println(" GeoArea.OVERLAPS: keep splitting"); |
| } |
| // They do overlap but neither contains the other: |
| //log.println(" crosses1"); |
| break; |
| case GeoArea.WITHIN: |
| if (VERBOSE) { |
| log.println(" GeoArea.WITHIN: keep splitting"); |
| } |
| // Cell fully contains the shape: |
| //log.println(" crosses2"); |
| break; |
| case GeoArea.DISJOINT: |
| // They do not overlap at all: don't recurse on this cell |
| //log.println(" outside"); |
| if (VERBOSE) { |
| log.println(" GeoArea.DISJOINT: drop this cell"); |
| for(int docID=0;docID<numDocs;docID++) { |
| if (cell.contains(planetMax, docs[docID])) { |
| if (VERBOSE) { |
| log.println(" skip doc=" + docID); |
| } |
| } |
| } |
| } |
| continue; |
| default: |
| assert false; |
| } |
| |
| // Randomly split: |
| switch(random().nextInt(3)) { |
| |
| case 0: |
| // Split on X: |
| { |
| int splitValue = RandomInts.randomIntBetween(random(), cell.xMinEnc, cell.xMaxEnc); |
| if (VERBOSE) { |
| log.println(" now split on x=" + splitValue); |
| } |
| Cell cell1 = new Cell(cell, |
| cell.xMinEnc, splitValue, |
| cell.yMinEnc, cell.yMaxEnc, |
| cell.zMinEnc, cell.zMaxEnc, |
| cell.splitCount+1); |
| Cell cell2 = new Cell(cell, |
| splitValue, cell.xMaxEnc, |
| cell.yMinEnc, cell.yMaxEnc, |
| cell.zMinEnc, cell.zMaxEnc, |
| cell.splitCount+1); |
| if (VERBOSE) { |
| log.println(" split cell1: " + cell1); |
| log.println(" split cell2: " + cell2); |
| } |
| queue.add(cell1); |
| queue.add(cell2); |
| } |
| break; |
| |
| case 1: |
| // Split on Y: |
| { |
| int splitValue = RandomInts.randomIntBetween(random(), cell.yMinEnc, cell.yMaxEnc); |
| if (VERBOSE) { |
| log.println(" now split on y=" + splitValue); |
| } |
| Cell cell1 = new Cell(cell, |
| cell.xMinEnc, cell.xMaxEnc, |
| cell.yMinEnc, splitValue, |
| cell.zMinEnc, cell.zMaxEnc, |
| cell.splitCount+1); |
| Cell cell2 = new Cell(cell, |
| cell.xMinEnc, cell.xMaxEnc, |
| splitValue, cell.yMaxEnc, |
| cell.zMinEnc, cell.zMaxEnc, |
| cell.splitCount+1); |
| if (VERBOSE) { |
| log.println(" split cell1: " + cell1); |
| log.println(" split cell2: " + cell2); |
| } |
| queue.add(cell1); |
| queue.add(cell2); |
| } |
| break; |
| |
| case 2: |
| // Split on Z: |
| { |
| int splitValue = RandomInts.randomIntBetween(random(), cell.zMinEnc, cell.zMaxEnc); |
| if (VERBOSE) { |
| log.println(" now split on z=" + splitValue); |
| } |
| Cell cell1 = new Cell(cell, |
| cell.xMinEnc, cell.xMaxEnc, |
| cell.yMinEnc, cell.yMaxEnc, |
| cell.zMinEnc, splitValue, |
| cell.splitCount+1); |
| Cell cell2 = new Cell(cell, |
| cell.xMinEnc, cell.xMaxEnc, |
| cell.yMinEnc, cell.yMaxEnc, |
| splitValue, cell.zMaxEnc, |
| cell.splitCount+1); |
| if (VERBOSE) { |
| log.println(" split cell1: " + cell1); |
| log.println(" split cell2: " + cell2); |
| } |
| queue.add(cell1); |
| queue.add(cell2); |
| } |
| break; |
| } |
| } |
| } |
| |
| if (VERBOSE) { |
| log.println(" " + hits.size() + " hits"); |
| } |
| |
| // Done matching, now verify: |
| boolean fail = false; |
| for(int docID=0;docID<numDocs;docID++) { |
| GeoPoint point = docs[docID]; |
| GeoPoint quantized = quantize(planetMax, point); |
| boolean expected = shape.isWithin(quantized); |
| |
| if (expected != shape.isWithin(point)) { |
| // Quantization changed the result; skip testing this doc: |
| continue; |
| } |
| |
| boolean actual = hits.contains(docID); |
| if (actual != expected) { |
| if (actual) { |
| log.println("doc=" + docID + " matched but should not"); |
| } else { |
| log.println("doc=" + docID + " did not match but should"); |
| } |
| log.println(" point=" + docs[docID]); |
| log.println(" quantized=" + quantize(planetMax, docs[docID])); |
| fail = true; |
| } |
| } |
| |
| if (fail) { |
| System.out.print(sw.toString()); |
| fail("invalid hits for shape=" + shape); |
| } |
| } |
| } |
| |
| public void testRandomTiny() throws Exception { |
| // Make sure single-leaf-node case is OK: |
| doTestRandom(10); |
| } |
| |
| public void testRandomMedium() throws Exception { |
| doTestRandom(10000); |
| } |
| |
| @Nightly |
| public void testRandomBig() throws Exception { |
| doTestRandom(200000); |
| } |
| |
| private void doTestRandom(int count) throws Exception { |
| int numPoints = atLeast(count); |
| |
| if (VERBOSE) { |
| System.err.println("TEST: numPoints=" + numPoints); |
| } |
| |
| double[] lats = new double[numPoints]; |
| double[] lons = new double[numPoints]; |
| |
| boolean haveRealDoc = false; |
| |
| for (int docID=0;docID<numPoints;docID++) { |
| int x = random().nextInt(20); |
| if (x == 17) { |
| // Some docs don't have a point: |
| lats[docID] = Double.NaN; |
| if (VERBOSE) { |
| System.err.println(" doc=" + docID + " is missing"); |
| } |
| continue; |
| } |
| |
| if (docID > 0 && x < 3 && haveRealDoc) { |
| int oldDocID; |
| while (true) { |
| oldDocID = random().nextInt(docID); |
| if (Double.isNaN(lats[oldDocID]) == false) { |
| break; |
| } |
| } |
| |
| if (x == 0) { |
| // Identical lat to old point |
| lats[docID] = lats[oldDocID]; |
| lons[docID] = toRadians(randomLon()); |
| if (VERBOSE) { |
| System.err.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID] + " (same lat as doc=" + oldDocID + ")"); |
| } |
| } else if (x == 1) { |
| // Identical lon to old point |
| lats[docID] = toRadians(randomLat()); |
| lons[docID] = lons[oldDocID]; |
| if (VERBOSE) { |
| System.err.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID] + " (same lon as doc=" + oldDocID + ")"); |
| } |
| } else { |
| assert x == 2; |
| // Fully identical point: |
| lats[docID] = lats[oldDocID]; |
| lons[docID] = lons[oldDocID]; |
| if (VERBOSE) { |
| System.err.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID] + " (same lat/lon as doc=" + oldDocID + ")"); |
| } |
| } |
| } else { |
| lats[docID] = toRadians(randomLat()); |
| lons[docID] = toRadians(randomLon()); |
| haveRealDoc = true; |
| if (VERBOSE) { |
| System.err.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID]); |
| } |
| } |
| } |
| |
| verify(lats, lons); |
| } |
| |
| private static double randomLat() { |
| if (smallBBox) { |
| return 2.0 * (random().nextDouble()-0.5); |
| } else { |
| return -90 + 180.0 * random().nextDouble(); |
| } |
| } |
| |
| private static double randomLon() { |
| if (smallBBox) { |
| return 2.0 * (random().nextDouble()-0.5); |
| } else { |
| return -180 + 360.0 * random().nextDouble(); |
| } |
| } |
| |
| // Poached from Geo3dRptTest.randomShape: |
| private static GeoShape randomShape(PlanetModel planetModel) { |
| while (true) { |
| final int shapeType = random().nextInt(4); |
| switch (shapeType) { |
| case 0: { |
| // Polygons |
| final int vertexCount = random().nextInt(3) + 3; |
| final List<GeoPoint> geoPoints = new ArrayList<>(); |
| while (geoPoints.size() < vertexCount) { |
| final GeoPoint gPt = new GeoPoint(planetModel, toRadians(randomLat()), toRadians(randomLon())); |
| geoPoints.add(gPt); |
| } |
| final int convexPointIndex = random().nextInt(vertexCount); //If we get this wrong, hopefully we get IllegalArgumentException |
| try { |
| return GeoPolygonFactory.makeGeoPolygon(planetModel, geoPoints, convexPointIndex); |
| } catch (IllegalArgumentException e) { |
| // This is what happens when we create a shape that is invalid. Although it is conceivable that there are cases where |
| // the exception is thrown incorrectly, we aren't going to be able to do that in this random test. |
| continue; |
| } |
| } |
| |
| case 1: { |
| // Circles |
| |
| double lat = toRadians(randomLat()); |
| double lon = toRadians(randomLon()); |
| |
| double angle; |
| if (smallBBox) { |
| angle = random().nextDouble() * Math.PI/360.0; |
| } else { |
| angle = random().nextDouble() * Math.PI/2.0; |
| } |
| |
| try { |
| return GeoCircleFactory.makeGeoCircle(planetModel, lat, lon, angle); |
| } catch (IllegalArgumentException iae) { |
| // angle is too small; try again: |
| continue; |
| } |
| } |
| |
| case 2: { |
| // Rectangles |
| double lat0 = toRadians(randomLat()); |
| double lat1 = toRadians(randomLat()); |
| if (lat1 < lat0) { |
| double x = lat0; |
| lat0 = lat1; |
| lat1 = x; |
| } |
| double lon0 = toRadians(randomLon()); |
| double lon1 = toRadians(randomLon()); |
| if (lon1 < lon0) { |
| double x = lon0; |
| lon0 = lon1; |
| lon1 = x; |
| } |
| |
| return GeoBBoxFactory.makeGeoBBox(planetModel, lat1, lat0, lon0, lon1); |
| } |
| |
| case 3: { |
| // Paths |
| final int pointCount = random().nextInt(5) + 1; |
| final double width = toRadians(random().nextInt(89)+1); |
| try { |
| final GeoPath path = new GeoPath(planetModel, width); |
| for (int i = 0; i < pointCount; i++) { |
| path.addPoint(toRadians(randomLat()), toRadians(randomLon())); |
| } |
| path.done(); |
| return path; |
| } catch (IllegalArgumentException e) { |
| // This is what happens when we create a shape that is invalid. Although it is conceivable that there are cases where |
| // the exception is thrown incorrectly, we aren't going to be able to do that in this random test. |
| continue; |
| } |
| } |
| |
| default: |
| throw new IllegalStateException("Unexpected shape type"); |
| } |
| } |
| } |
| |
| private static void verify(double[] lats, double[] lons) throws Exception { |
| IndexWriterConfig iwc = newIndexWriterConfig(); |
| |
| // Else we can get O(N^2) merging: |
| int mbd = iwc.getMaxBufferedDocs(); |
| if (mbd != -1 && mbd < lats.length/100) { |
| iwc.setMaxBufferedDocs(lats.length/100); |
| } |
| iwc.setCodec(getCodec()); |
| Directory dir; |
| if (lats.length > 100000) { |
| dir = newFSDirectory(createTempDir("TestBKDTree")); |
| } else { |
| dir = getDirectory(); |
| } |
| Set<Integer> deleted = new HashSet<>(); |
| // RandomIndexWriter is too slow here: |
| IndexWriter w = new IndexWriter(dir, iwc); |
| for(int id=0;id<lats.length;id++) { |
| Document doc = new Document(); |
| doc.add(newStringField("id", ""+id, Field.Store.NO)); |
| doc.add(new NumericDocValuesField("id", id)); |
| if (Double.isNaN(lats[id]) == false) { |
| doc.add(new Geo3DPoint("point", lats[id], lons[id])); |
| } |
| w.addDocument(doc); |
| if (id > 0 && random().nextInt(100) == 42) { |
| int idToDelete = random().nextInt(id); |
| w.deleteDocuments(new Term("id", ""+idToDelete)); |
| deleted.add(idToDelete); |
| if (VERBOSE) { |
| System.err.println(" delete id=" + idToDelete); |
| } |
| } |
| } |
| if (random().nextBoolean()) { |
| w.forceMerge(1); |
| } |
| final IndexReader r = DirectoryReader.open(w); |
| w.close(); |
| |
| // We can't wrap with "exotic" readers because the geo3d query must see the Geo3DDVFormat: |
| IndexSearcher s = newSearcher(r, false); |
| |
| int numThreads = TestUtil.nextInt(random(), 2, 5); |
| |
| List<Thread> threads = new ArrayList<>(); |
| final int iters = atLeast(100); |
| |
| final CountDownLatch startingGun = new CountDownLatch(1); |
| final AtomicBoolean failed = new AtomicBoolean(); |
| |
| for(int i=0;i<numThreads;i++) { |
| Thread thread = new Thread() { |
| @Override |
| public void run() { |
| try { |
| _run(); |
| } catch (Exception e) { |
| failed.set(true); |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private void _run() throws Exception { |
| startingGun.await(); |
| |
| NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id"); |
| |
| for (int iter=0;iter<iters && failed.get() == false;iter++) { |
| |
| GeoShape shape = randomShape(PlanetModel.WGS84); |
| |
| if (VERBOSE) { |
| System.err.println("\n" + Thread.currentThread() + ": TEST: iter=" + iter + " shape="+shape); |
| } |
| |
| Query query = Geo3DPoint.newShapeQuery("point", shape); |
| |
| if (VERBOSE) { |
| System.err.println(" using query: " + query); |
| } |
| |
| final FixedBitSet hits = new FixedBitSet(r.maxDoc()); |
| |
| s.search(query, new SimpleCollector() { |
| |
| private int docBase; |
| |
| @Override |
| public boolean needsScores() { |
| return false; |
| } |
| |
| @Override |
| protected void doSetNextReader(LeafReaderContext context) throws IOException { |
| docBase = context.docBase; |
| } |
| |
| @Override |
| public void collect(int doc) { |
| hits.set(docBase+doc); |
| } |
| }); |
| |
| if (VERBOSE) { |
| System.err.println(" hitCount: " + hits.cardinality()); |
| } |
| |
| for(int docID=0;docID<r.maxDoc();docID++) { |
| int id = (int) docIDToID.get(docID); |
| if (Double.isNaN(lats[id]) == false) { |
| |
| // Accurate point: |
| GeoPoint point1 = new GeoPoint(PlanetModel.WGS84, lats[id], lons[id]); |
| |
| // Quantized point (32 bits per dim): |
| GeoPoint point2 = quantize(PlanetModel.WGS84.getMaximumMagnitude(), point1); |
| |
| if (shape.isWithin(point1) != shape.isWithin(point2)) { |
| if (VERBOSE) { |
| System.out.println(" skip checking docID=" + docID + " quantization changed the expected result from " + shape.isWithin(point1) + " to " + shape.isWithin(point2)); |
| } |
| continue; |
| } |
| |
| boolean expected = ((deleted.contains(id) == false) && shape.isWithin(point2)); |
| if (hits.get(docID) != expected) { |
| fail(Thread.currentThread().getName() + ": iter=" + iter + " id=" + id + " docID=" + docID + " lat=" + lats[id] + " lon=" + lons[id] + " expected " + expected + " but got: " + hits.get(docID) + " deleted?=" + deleted.contains(id) + "\n point1=" + point1 + ", iswithin="+shape.isWithin(point1)+"\n point2=" + point2 + ", iswithin="+shape.isWithin(point2) + "\n query=" + query); |
| } |
| } else { |
| assertFalse(hits.get(docID)); |
| } |
| |
| } |
| } |
| } |
| }; |
| thread.setName("T" + i); |
| thread.start(); |
| threads.add(thread); |
| } |
| startingGun.countDown(); |
| for(Thread thread : threads) { |
| thread.join(); |
| } |
| IOUtils.close(r, dir); |
| } |
| |
| public void testToString() { |
| Geo3DPoint point = new Geo3DPoint("point", toRadians(44.244272), toRadians(7.769736)); |
| assertEquals("Geo3DPoint <point: x=0.9248468196007059 y=0.06280434490718728 z=0.37682350357577465>", point.toString()); |
| } |
| |
| public void testShapeQueryToString() { |
| assertEquals("PointInGeo3DShapeQuery: field=point: Shape: GeoStandardCircle: {planetmodel=PlanetModel.WGS84, center=[lat=0.3861041107739683, lon=0.06780373760536706], radius=0.1(5.729577951308232)}", |
| Geo3DPoint.newShapeQuery("point", GeoCircleFactory.makeGeoCircle(PlanetModel.WGS84, toRadians(44.244272), toRadians(7.769736), 0.1)).toString()); |
| } |
| |
| private static Directory getDirectory() { |
| return newDirectory(); |
| } |
| |
| public void testEquals() { |
| GeoShape shape = randomShape(PlanetModel.WGS84); |
| Query q = Geo3DPoint.newShapeQuery("point", shape); |
| assertEquals(q, Geo3DPoint.newShapeQuery("point", shape)); |
| assertFalse(q.equals(Geo3DPoint.newShapeQuery("point2", shape))); |
| |
| // make a different random shape: |
| GeoShape shape2; |
| do { |
| shape2 = randomShape(PlanetModel.WGS84); |
| } while (shape.equals(shape2)); |
| |
| assertFalse(q.equals(Geo3DPoint.newShapeQuery("point", shape2))); |
| } |
| } |