blob: 2d8fd49322c32d85f5586259415782da60261433 [file] [log] [blame]
/*
* 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.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
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.lucene86.Lucene86PointsReader;
import org.apache.lucene.codecs.lucene86.Lucene86PointsWriter;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.geo.GeoTestUtil;
import org.apache.lucene.geo.Polygon;
import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.MultiDocValues;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.index.PointValues.IntersectVisitor;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.index.ReaderUtil;
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.ScoreMode;
import org.apache.lucene.search.SimpleCollector;
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.GeoPathFactory;
import org.apache.lucene.spatial3d.geom.GeoPoint;
import org.apache.lucene.spatial3d.geom.GeoPolygon;
import org.apache.lucene.spatial3d.geom.GeoPolygonFactory;
import org.apache.lucene.spatial3d.geom.GeoShape;
import org.apache.lucene.spatial3d.geom.Plane;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import org.apache.lucene.spatial3d.geom.SidedPlane;
import org.apache.lucene.spatial3d.geom.XYZBounds;
import org.apache.lucene.spatial3d.geom.XYZSolid;
import org.apache.lucene.spatial3d.geom.XYZSolidFactory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.DocIdSetBuilder;
import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.FutureArrays;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.NumericUtils;
import org.apache.lucene.util.TestUtil;
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
public class TestGeo3DPoint extends LuceneTestCase {
protected PlanetModel randomPlanetModel() {
return RandomPicks.randomFrom(random(), new PlanetModel[] {PlanetModel.WGS84, PlanetModel.CLARKE_1866, PlanetModel.SPHERE});
}
private static Codec getCodec() {
if (Codec.getDefault().getName().equals("Lucene84")) {
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("Lucene84", Codec.getDefault()) {
@Override
public PointsFormat pointsFormat() {
return new PointsFormat() {
@Override
public PointsWriter fieldsWriter(SegmentWriteState writeState) throws IOException {
return new Lucene86PointsWriter(writeState, maxPointsInLeafNode, maxMBSortInHeap);
}
@Override
public PointsReader fieldsReader(SegmentReadState readState) throws IOException {
return new Lucene86PointsReader(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();
PlanetModel planetModel = randomPlanetModel();
doc.add(new Geo3DPoint("field", planetModel, 50.7345267, -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, toRadians(50), toRadians(-97), Math.PI/180.)), 1).totalHits.value);
w.close();
r.close();
dir.close();
}
private static double toRadians(double degrees) {
return degrees * Geo3DUtil.RADIANS_PER_DEGREE;
}
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;
final PlanetModel planetModel;
public Cell(Cell parent,
int xMinEnc, int xMaxEnc,
int yMinEnc, int yMaxEnc,
int zMinEnc, int zMaxEnc,
final PlanetModel planetModel,
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;
this.planetModel = planetModel;
}
/** Returns true if the quantized point lies within this cell, inclusive on all bounds. */
public boolean contains(GeoPoint point) {
int docX = planetModel.encodeValue(point.x);
int docY = planetModel.encodeValue(point.y);
int docZ = planetModel.encodeValue(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 double quantize(double xyzValue, final PlanetModel planetModel) {
return planetModel.decodeValue(planetModel.encodeValue(xyzValue));
}
private static GeoPoint quantize(GeoPoint point, final PlanetModel planetModel) {
return new GeoPoint(quantize(point.x, planetModel), quantize(point.y, planetModel), quantize(point.z, planetModel));
}
/** Tests consistency of GeoArea.getRelationship vs GeoShape.isWithin */
public void testGeo3DRelations() throws Exception {
int numDocs = atLeast(200);
if (VERBOSE) {
System.out.println("TEST: " + numDocs + " docs");
}
GeoPoint[] docs = new GeoPoint[numDocs];
GeoPoint[] unquantizedDocs = new GeoPoint[numDocs];
PlanetModel planetModel = PlanetModel.CLARKE_1866;
for(int docID=0;docID<numDocs;docID++) {
unquantizedDocs[docID] = new GeoPoint(planetModel, toRadians(GeoTestUtil.nextLatitude()), toRadians(GeoTestUtil.nextLongitude()));
docs[docID] = quantize(unquantizedDocs[docID], planetModel);
if (VERBOSE) {
System.out.println(" doc=" + docID + ": " + docs[docID] + "; unquantized: "+unquantizedDocs[docID]);
}
}
int iters = atLeast(10);
int recurseDepth = RandomNumbers.randomIntBetween(random(), 5, 15);
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,
encodeValueLenient(bounds.getMinimumX(), planetModel),
encodeValueLenient(bounds.getMaximumX(), planetModel),
encodeValueLenient(bounds.getMinimumY(), planetModel),
encodeValueLenient(bounds.getMaximumY(), planetModel),
encodeValueLenient(bounds.getMinimumZ(), planetModel),
encodeValueLenient(bounds.getMaximumZ(), planetModel),
planetModel,
0);
if (VERBOSE) {
log.println(" root cell: " + root);
}
// make sure the root cell (XYZBounds) does in fact contain all points that the shape contains
{
boolean fail = false;
for(int docID=0;docID<numDocs;docID++) {
if (root.contains(docs[docID]) == false) {
boolean expected = shape.isWithin(unquantizedDocs[docID]);
if (expected) {
log.println(" doc=" + docID + " is contained by shape but is outside the returned XYZBounds");
log.println(" unquantized=" + unquantizedDocs[docID]);
log.println(" quantized=" + docs[docID]);
fail = true;
}
}
}
if (fail) {
log.println(" shape=" + shape);
log.println(" bounds=" + bounds);
System.out.print(sw.toString());
fail("invalid bounds for shape=" + shape);
}
}
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];
GeoPoint mappedPoint = unquantizedDocs[docID];
boolean pointWithinShape = shape.isWithin(point);
boolean mappedPointWithinShape = shape.isWithin(mappedPoint);
if (cell.contains(point)) {
if (mappedPointWithinShape) {
if (VERBOSE) {
log.println(" check doc=" + docID + ": match! Actual quantized point within: "+pointWithinShape);
}
hits.add(docID);
} else {
if (VERBOSE) {
log.println(" check doc=" + docID + ": no match. Quantized point within: "+pointWithinShape);
}
}
}
}
} else {
GeoArea xyzSolid = GeoAreaFactory.makeGeoArea(planetModel,
Geo3DUtil.decodeValueFloor(cell.xMinEnc, planetModel), Geo3DUtil.decodeValueCeil(cell.xMaxEnc, planetModel),
Geo3DUtil.decodeValueFloor(cell.yMinEnc, planetModel), Geo3DUtil.decodeValueCeil(cell.yMaxEnc, planetModel),
Geo3DUtil.decodeValueFloor(cell.zMinEnc, planetModel), Geo3DUtil.decodeValueCeil(cell.zMaxEnc, planetModel));
if (VERBOSE) {
log.println(" minx="+Geo3DUtil.decodeValueFloor(cell.xMinEnc, planetModel)+" maxx="+Geo3DUtil.decodeValueCeil(cell.xMaxEnc, planetModel)+
" miny="+Geo3DUtil.decodeValueFloor(cell.yMinEnc, planetModel)+" maxy="+Geo3DUtil.decodeValueCeil(cell.yMaxEnc, planetModel)+
" minz="+Geo3DUtil.decodeValueFloor(cell.zMinEnc, planetModel)+" maxz="+Geo3DUtil.decodeValueCeil(cell.zMaxEnc, planetModel));
}
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(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(docs[docID])) {
log.println(" skip doc=" + docID);
}
}
}
continue;
default:
assert false;
}
// Randomly split:
switch(random().nextInt(3)) {
case 0:
// Split on X:
{
int splitValue = RandomNumbers.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,
planetModel,
cell.splitCount+1);
Cell cell2 = new Cell(cell,
splitValue, cell.xMaxEnc,
cell.yMinEnc, cell.yMaxEnc,
cell.zMinEnc, cell.zMaxEnc,
planetModel,
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 = RandomNumbers.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,
planetModel,
cell.splitCount+1);
Cell cell2 = new Cell(cell,
cell.xMinEnc, cell.xMaxEnc,
splitValue, cell.yMaxEnc,
cell.zMinEnc, cell.zMaxEnc,
planetModel,
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 = RandomNumbers.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,
planetModel,
cell.splitCount+1);
Cell cell2 = new Cell(cell,
cell.xMinEnc, cell.xMaxEnc,
cell.yMinEnc, cell.yMaxEnc,
splitValue, cell.zMaxEnc,
planetModel,
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 mappedPoint = unquantizedDocs[docID];
boolean expected = shape.isWithin(mappedPoint);
boolean actual = hits.contains(docID);
if (actual != expected) {
if (actual) {
log.println("doc=" + docID + " should not have matched but did");
} else {
log.println("doc=" + docID + " should match but did not");
}
log.println(" point=" + point);
log.println(" mappedPoint=" + mappedPoint);
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(1000);
}
@Nightly
public void testRandomBig() throws Exception {
doTestRandom(50000);
}
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] = GeoTestUtil.nextLongitude();
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] = GeoTestUtil.nextLatitude();
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] = GeoTestUtil.nextLatitude();
lons[docID] = GeoTestUtil.nextLongitude();
haveRealDoc = true;
if (VERBOSE) {
System.err.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID]);
}
}
}
verify(lats, lons, randomPlanetModel());
}
public void testPolygonOrdering() {
final double[] lats = new double[] {
51.204382859999996, 50.89947531437482, 50.8093624806861,50.8093624806861, 50.89947531437482, 51.204382859999996, 51.51015366140113, 51.59953838204167, 51.59953838204167, 51.51015366140113, 51.204382859999996};
final double[] lons = new double[] {
0.8747711978759765, 0.6509219832137298, 0.35960265165247807, 0.10290284834752167, -0.18841648321373008, -0.41226569787597667, -0.18960465285650027, 0.10285893781346236, 0.35964656218653757, 0.6521101528565002, 0.8747711978759765};
final Query q = Geo3DPoint.newPolygonQuery("point", randomPlanetModel(), new Polygon(lats, lons));
//System.out.println(q);
assertTrue(!q.toString().contains("GeoConcavePolygon"));
}
private static Query random3DQuery(final String field, final PlanetModel planetModel) {
while (true) {
final int shapeType = random().nextInt(5);
switch (shapeType) {
case 4: {
// Large polygons
final boolean isClockwise = random().nextDouble() < 0.5;
try {
final Query q = Geo3DPoint.newLargePolygonQuery(field, planetModel, makePoly(planetModel,
new GeoPoint(planetModel, toRadians(GeoTestUtil.nextLatitude()), toRadians(GeoTestUtil.nextLongitude())),
isClockwise,
true));
//System.err.println("Generated: "+q);
//assertTrue(false);
return q;
} catch (IllegalArgumentException e) {
continue;
}
}
case 0: {
// Polygons
final boolean isClockwise = random().nextDouble() < 0.5;
try {
final Query q = Geo3DPoint.newPolygonQuery(field, planetModel, makePoly(planetModel,
new GeoPoint(planetModel, toRadians(GeoTestUtil.nextLatitude()), toRadians(GeoTestUtil.nextLongitude())),
isClockwise,
true));
//System.err.println("Generated: "+q);
//assertTrue(false);
return q;
} catch (IllegalArgumentException e) {
continue;
}
}
case 1: {
// Circles
final double widthMeters = random().nextDouble() * Math.PI * planetModel.getMeanRadius();
try {
return Geo3DPoint.newDistanceQuery(field, planetModel, GeoTestUtil.nextLatitude(), GeoTestUtil.nextLongitude(), widthMeters);
} catch (IllegalArgumentException e) {
continue;
}
}
case 2: {
// Rectangles
final Rectangle r = GeoTestUtil.nextBox();
try {
return Geo3DPoint.newBoxQuery(field, planetModel, r.minLat, r.maxLat, r.minLon, r.maxLon);
} catch (IllegalArgumentException e) {
continue;
}
}
case 3: {
// Paths
// TBD: Need to rework generation to be realistic
final int pointCount = random().nextInt(5) + 1;
final double width = random().nextDouble() * Math.PI * 0.5 * planetModel.getMeanRadius();
final double[] latitudes = new double[pointCount];
final double[] longitudes = new double[pointCount];
for (int i = 0; i < pointCount; i++) {
latitudes[i] = GeoTestUtil.nextLatitude();
longitudes[i] = GeoTestUtil.nextLongitude();
}
try {
return Geo3DPoint.newPathQuery(field, latitudes, longitudes, width, planetModel);
} 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");
}
}
}
// Poached from Geo3dRptTest.randomShape:
private static GeoShape randomShape(final 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(GeoTestUtil.nextLatitude()), toRadians(GeoTestUtil.nextLongitude()));
geoPoints.add(gPt);
}
try {
final GeoShape rval = GeoPolygonFactory.makeGeoPolygon(planetModel, geoPoints);
if (rval == null) {
// Degenerate polygon
continue;
}
return rval;
} 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(GeoTestUtil.nextLatitude());
double lon = toRadians(GeoTestUtil.nextLongitude());
double 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(GeoTestUtil.nextLatitude());
double lat1 = toRadians(GeoTestUtil.nextLatitude());
if (lat1 < lat0) {
double x = lat0;
lat0 = lat1;
lat1 = x;
}
double lon0 = toRadians(GeoTestUtil.nextLongitude());
double lon1 = toRadians(GeoTestUtil.nextLongitude());
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);
final GeoPoint[] points = new GeoPoint[pointCount];
for (int i = 0; i < pointCount; i++) {
points[i] = new GeoPoint(planetModel, toRadians(GeoTestUtil.nextLatitude()), toRadians(GeoTestUtil.nextLongitude()));
}
try {
return GeoPathFactory.makeGeoPath(planetModel, width, points);
} 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, final PlanetModel planetModel) throws Exception {
IndexWriterConfig iwc = newIndexWriterConfig();
GeoPoint[] points = new GeoPoint[lats.length];
GeoPoint[] unquantizedPoints = new GeoPoint[lats.length];
// Pre-quantize all lat/lons:
for(int i=0;i<lats.length;i++) {
if (Double.isNaN(lats[i]) == false) {
//System.out.println("lats[" + i + "] = " + lats[i]);
unquantizedPoints[i] = new GeoPoint(planetModel, toRadians(lats[i]), toRadians(lons[i]));
points[i] = quantize(unquantizedPoints[i], planetModel);
}
}
// Else we can get O(N^2) merging:
int mbd = iwc.getMaxBufferedDocs();
if (mbd != -1 && mbd < points.length/100) {
iwc.setMaxBufferedDocs(points.length/100);
}
iwc.setCodec(getCodec());
Directory dir;
if (points.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<points.length;id++) {
Document doc = new Document();
doc.add(newStringField("id", ""+id, Field.Store.NO));
doc.add(new NumericDocValuesField("id", id));
GeoPoint point = points[id];
if (point != null) {
doc.add(new Geo3DPoint("point", planetModel, point.x, point.y, point.z));
}
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);
if (VERBOSE) {
System.out.println("TEST: using reader " + r);
}
w.close();
// We can't wrap with "exotic" readers because the geo3d query must see the Geo3DDVFormat:
IndexSearcher s = newSearcher(r, false);
final int iters = atLeast(100);
for (int iter=0;iter<iters;iter++) {
/*
GeoShape shape = randomShape();
if (VERBOSE) {
System.err.println("\nTEST: iter=" + iter + " shape="+shape);
}
*/
Query query = random3DQuery("point", planetModel); // 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 ScoreMode scoreMode() {
return ScoreMode.COMPLETE_NO_SCORES;
}
@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());
}
NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id");
for(int docID=0;docID<r.maxDoc();docID++) {
assertEquals(docID, docIDToID.nextDoc());
int id = (int) docIDToID.longValue();
GeoPoint point = points[id];
GeoPoint unquantizedPoint = unquantizedPoints[id];
if (point != null && unquantizedPoint != null) {
GeoShape shape = ((PointInGeo3DShapeQuery)query).getShape();
XYZBounds bounds = new XYZBounds();
shape.getBounds(bounds);
XYZSolid solid = XYZSolidFactory.makeXYZSolid(planetModel, bounds.getMinimumX(), bounds.getMaximumX(), bounds.getMinimumY(), bounds.getMaximumY(), bounds.getMinimumZ(), bounds.getMaximumZ());
boolean expected = ((deleted.contains(id) == false) && shape.isWithin(point));
if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder();
if (expected) {
b.append("FAIL: id=" + id + " should have matched but did not\n");
} else {
b.append("FAIL: id=" + id + " should not have matched but did\n");
}
b.append(" shape=" + shape + "\n");
b.append(" bounds=" + bounds + "\n");
b.append(" world bounds=(" +
" minX=" + planetModel.getMinimumXValue() + " maxX=" + planetModel.getMaximumXValue() +
" minY=" + planetModel.getMinimumYValue() + " maxY=" + planetModel.getMaximumYValue() +
" minZ=" + planetModel.getMinimumZValue() + " maxZ=" + planetModel.getMaximumZValue() + "\n");
b.append(" quantized point=" + point + " within shape? "+shape.isWithin(point)+" within bounds? "+solid.isWithin(point)+"\n");
b.append(" unquantized point=" + unquantizedPoint + " within shape? "+shape.isWithin(unquantizedPoint)+" within bounds? "+solid.isWithin(unquantizedPoint)+"\n");
b.append(" docID=" + docID + " deleted?=" + deleted.contains(id) + "\n");
b.append(" query=" + query + "\n");
b.append(" explanation:\n " + explain("point", shape, point, unquantizedPoint, r, docID).replace("\n", "\n "));
fail(b.toString());
}
} else {
assertFalse(hits.get(docID));
}
}
}
IOUtils.close(r, dir);
}
public void testToString() {
// Don't compare entire strings because Java 9 and Java 8 have slightly different values
Geo3DPoint point = new Geo3DPoint("point", randomPlanetModel(), 44.244272, 7.769736);
final String stringToCompare = "Geo3DPoint <point: x=";
assertEquals(stringToCompare, point.toString().substring(0,stringToCompare.length()));
}
public void testShapeQueryToString() {
// Don't compare entire strings because Java 9 and Java 8 have slightly different values
final String stringToCompare = "PointInGeo3DShapeQuery: field=point: Shape: GeoStandardCircle: {planetmodel=PlanetModel.WGS84, center=[lat=0.7";
assertEquals(stringToCompare,
Geo3DPoint.newShapeQuery("point", GeoCircleFactory.makeGeoCircle(PlanetModel.WGS84, toRadians(44.244272), toRadians(7.769736), 0.1)).toString().substring(0,stringToCompare.length()));
}
private static Directory getDirectory() {
return newDirectory();
}
public void testEquals() {
PlanetModel planetModel = randomPlanetModel();
GeoShape shape = randomShape(planetModel);
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);
} while (shape.equals(shape2));
assertFalse(q.equals(Geo3DPoint.newShapeQuery("point", shape2)));
}
public void testComplexPolygons() {
final PlanetModel pm = randomPlanetModel();
// Pick a random pole
final GeoPoint randomPole = new GeoPoint(pm, toRadians(GeoTestUtil.nextLatitude()), toRadians(GeoTestUtil.nextLongitude()));
int iters = atLeast(100);
for (int i = 0; i < iters; i++) {
// Create a polygon that's less than 180 degrees
final Polygon clockWise = makePoly(pm, randomPole, true, true);
}
iters = atLeast(100);
for (int i = 0; i < iters; i++) {
// Create a polygon that's greater than 180 degrees
final Polygon counterClockWise = makePoly(pm, randomPole, false, true);
}
}
protected static double MINIMUM_EDGE_ANGLE = toRadians(5.0);
protected static double MINIMUM_ARC_ANGLE = toRadians(1.0);
/** Cook up a random Polygon that makes sense, with possible nested polygon within.
* This is part of testing more complex polygons with nested holes. Picking random points
* doesn't do it because it's almost impossible to come up with nested ones of the proper
* clockwise/counterclockwise rotation that way.
*/
protected static Polygon makePoly(final PlanetModel pm, final GeoPoint pole, final boolean clockwiseDesired, final boolean createHoles) {
// Polygon edges will be arranged around the provided pole, and holes will each have a pole selected within the parent
// polygon.
final int pointCount = TestUtil.nextInt(random(), 3, 10);
// The point angles we pick next. The only requirement is that they are not all on one side of the pole.
// We arrange that by picking the next point within what's left of the remaining angle, but never more than 180 degrees,
// and never less than what is needed to insure that the remaining point choices are less than 180 degrees always.
// These are all picked in the context of the pole,
final double[] angles = new double[pointCount];
final double[] arcDistance = new double[pointCount];
// Pick a set of points
while (true) {
double accumulatedAngle = 0.0;
for (int i = 0; i < pointCount; i++) {
final int remainingEdgeCount = pointCount - i;
final double remainingAngle = 2.0 * Math.PI - accumulatedAngle;
if (remainingEdgeCount == 1) {
angles[i] = remainingAngle;
} else {
// The maximum angle is 180 degrees, or what's left when you give a minimal amount to each edge.
double maximumAngle = remainingAngle - (remainingEdgeCount-1) * MINIMUM_EDGE_ANGLE;
if (maximumAngle > Math.PI) {
maximumAngle = Math.PI;
}
// The minimum angle is MINIMUM_EDGE_ANGLE, or enough to be sure nobody afterwards needs more than
// 180 degrees. And since we have three points to start with, we already know that.
final double minimumAngle = MINIMUM_EDGE_ANGLE;
// Pick the angle
final double angle = random().nextDouble() * (maximumAngle - minimumAngle) + minimumAngle;
angles[i] = angle;
accumulatedAngle += angle;
}
// Pick the arc distance randomly; not quite the full range though
arcDistance[i] = random().nextDouble() * (Math.PI * 0.5 - MINIMUM_ARC_ANGLE) + MINIMUM_ARC_ANGLE;
}
if (clockwiseDesired) {
// Reverse the signs
for (int i = 0; i < pointCount; i++) {
angles[i] = -angles[i];
}
}
// Now, use the pole's information plus angles and arcs to create GeoPoints in the right order.
final List<GeoPoint> polyPoints = convertToPoints(pm, pole, angles, arcDistance);
// Next, do some holes. No more than 2 of these. The poles for holes must always be within the polygon, so we're
// going to use Geo3D to help us select those given the points we just made.
final int holeCount = createHoles?TestUtil.nextInt(random(), 0, 2):0;
final List<Polygon> holeList = new ArrayList<>();
/* Hole logic is broken and needs rethinking
// Create the geo3d polygon, so we can test out our poles.
final GeoPolygon poly;
try {
poly = GeoPolygonFactory.makeGeoPolygon(pm, polyPoints, null);
} catch (IllegalArgumentException e) {
// This is what happens when three adjacent points are colinear, so try again.
continue;
}
for (int i = 0; i < holeCount; i++) {
// Choose a pole. The poly has to be within the polygon, but it also cannot be on the polygon edge.
// If we can't find a good pole we have to give it up and not do the hole.
for (int k = 0; k < 500; k++) {
final GeoPoint poleChoice = new GeoPoint(pm, toRadians(GeoTestUtil.nextLatitude()), toRadians(GeoTestUtil.nextLongitude()));
if (!poly.isWithin(poleChoice)) {
continue;
}
// We have a pole within the polygon. Now try 100 times to build a polygon that does not intersect the outside ring.
// After that we give up and pick a new pole.
boolean foundOne = false;
for (int j = 0; j < 100; j++) {
final Polygon insidePoly = makePoly(pm, poleChoice, !clockwiseDesired, false);
// Verify that the inside polygon is OK. If not, discard and repeat.
if (!verifyPolygon(pm, insidePoly, poly)) {
continue;
}
holeList.add(insidePoly);
foundOne = true;
}
if (foundOne) {
break;
}
}
}
*/
final Polygon[] holes = holeList.toArray(new Polygon[0]);
// Finally, build the polygon and return it
final double[] lats = new double[polyPoints.size() + 1];
final double[] lons = new double[polyPoints.size() + 1];
for (int i = 0; i < polyPoints.size(); i++) {
lats[i] = polyPoints.get(i).getLatitude() * 180.0 / Math.PI;
lons[i] = polyPoints.get(i).getLongitude() * 180.0 / Math.PI;
}
lats[polyPoints.size()] = lats[0];
lons[polyPoints.size()] = lons[0];
return new Polygon(lats, lons, holes);
}
}
protected static List<GeoPoint> convertToPoints(final PlanetModel pm, final GeoPoint pole, final double[] angles, final double[] arcDistances) {
// To do the point rotations, we need the sine and cosine of the pole latitude and longitude. Get it here for performance.
final double sinLatitude = Math.sin(pole.getLatitude());
final double cosLatitude = Math.cos(pole.getLatitude());
final double sinLongitude = Math.sin(pole.getLongitude());
final double cosLongitude = Math.cos(pole.getLongitude());
final List<GeoPoint> rval = new ArrayList<>();
for (int i = 0; i < angles.length; i++) {
rval.add(createPoint(pm, angles[i], arcDistances[i], sinLatitude, cosLatitude, sinLongitude, cosLongitude));
}
return rval;
}
protected static GeoPoint createPoint(final PlanetModel pm,
final double angle,
final double arcDistance,
final double sinLatitude,
final double cosLatitude,
final double sinLongitude,
final double cosLongitude) {
// From the angle and arc distance, convert to (x,y,z) in unit space.
// We want the perspective to be looking down the x axis. The "angle" measurement is thus in the Y-Z plane.
// The arcdistance is in X.
final double x = Math.cos(arcDistance);
final double yzScale = Math.sin(arcDistance);
final double y = Math.cos(angle) * yzScale;
final double z = Math.sin(angle) * yzScale;
// Now, rotate coordinates so that we shift everything from pole = x-axis to actual coordinates.
// This transformation should take the point (1,0,0) and transform it to the pole's actual (x,y,z) coordinates.
// Coordinate rotation formula:
// x1 = x0 cos T - y0 sin T
// y1 = x0 sin T + y0 cos T
// We're in essence undoing the following transformation (from GeoPolygonFactory):
// x1 = x0 cos az + y0 sin az
// y1 = - x0 sin az + y0 cos az
// z1 = z0
// x2 = x1 cos al + z1 sin al
// y2 = y1
// z2 = - x1 sin al + z1 cos al
// So, we reverse the order of the transformations, AND we transform backwards.
// Transforming backwards means using these identities: sin(-angle) = -sin(angle), cos(-angle) = cos(angle)
// So:
// x1 = x0 cos al - z0 sin al
// y1 = y0
// z1 = x0 sin al + z0 cos al
// x2 = x1 cos az - y1 sin az
// y2 = x1 sin az + y1 cos az
// z2 = z1
final double x1 = x * cosLatitude - z * sinLatitude;
final double y1 = y;
final double z1 = x * sinLatitude + z * cosLatitude;
final double x2 = x1 * cosLongitude - y1 * sinLongitude;
final double y2 = x1 * sinLongitude + y1 * cosLongitude;
final double z2 = z1;
// Scale to put the point on the surface
return pm.createSurfacePoint(x2, y2, z2);
}
protected static boolean verifyPolygon(final PlanetModel pm, final Polygon polygon, final GeoPolygon outsidePolygon) {
// Each point in the new poly should be inside the outside poly, and each edge should not intersect the outside poly edge
final double[] lats = polygon.getPolyLats();
final double[] lons = polygon.getPolyLons();
final List<GeoPoint> polyPoints = new ArrayList<>(lats.length-1);
for (int i = 0; i < lats.length - 1; i++) {
final GeoPoint newPoint = new GeoPoint(pm, toRadians(lats[i]), toRadians(lons[i]));
if (!outsidePolygon.isWithin(newPoint)) {
return false;
}
polyPoints.add(newPoint);
}
// We don't need to construct the world to find intersections -- just the bordering planes.
for (int planeIndex = 0; planeIndex < polyPoints.size(); planeIndex++) {
final GeoPoint startPoint = polyPoints.get(planeIndex);
final GeoPoint endPoint = polyPoints.get(legalIndex(planeIndex + 1, polyPoints.size()));
final GeoPoint beforeStartPoint = polyPoints.get(legalIndex(planeIndex - 1, polyPoints.size()));
final GeoPoint afterEndPoint = polyPoints.get(legalIndex(planeIndex + 2, polyPoints.size()));
final SidedPlane beforePlane = new SidedPlane(endPoint, beforeStartPoint, startPoint);
final SidedPlane afterPlane = new SidedPlane(startPoint, endPoint, afterEndPoint);
final Plane plane = new Plane(startPoint, endPoint);
// Check for intersections!!
if (outsidePolygon.intersects(plane, null, beforePlane, afterPlane)) {
return false;
}
}
return true;
}
protected static int legalIndex(int index, int size) {
if (index >= size) {
index -= size;
}
if (index < 0) {
index += size;
}
return index;
}
public void testEncodeDecodeCeil() throws Exception {
PlanetModel planetModel = randomPlanetModel();
// just for testing quantization error
final double ENCODING_TOLERANCE = planetModel.DECODE;
int iters = atLeast(10000);
for(int iter=0;iter<iters;iter++) {
GeoPoint point = new GeoPoint(planetModel, toRadians(GeoTestUtil.nextLatitude()), toRadians(GeoTestUtil.nextLongitude()));
double xEnc = planetModel.decodeValue(planetModel.encodeValue(point.x));
assertEquals("x=" + point.x + " xEnc=" + xEnc + " diff=" + (point.x - xEnc), point.x, xEnc, ENCODING_TOLERANCE);
double yEnc = planetModel.decodeValue(planetModel.encodeValue(point.y));
assertEquals("y=" + point.y + " yEnc=" + yEnc + " diff=" + (point.y - yEnc), point.y, yEnc, ENCODING_TOLERANCE);
double zEnc = planetModel.decodeValue(planetModel.encodeValue(point.z));
assertEquals("z=" + point.z + " zEnc=" + zEnc + " diff=" + (point.z - zEnc), point.z, zEnc, ENCODING_TOLERANCE);
}
// check edge/interesting cases explicitly
double planetMax = planetModel.getMaximumMagnitude();
for (double value : new double[] {0.0, -planetMax, planetMax}) {
assertEquals(value, planetModel.decodeValue(planetModel.encodeValue(value)), ENCODING_TOLERANCE);
}
}
/** make sure values always go down: this is important for edge case consistency */
public void testEncodeDecodeRoundsDown() throws Exception {
PlanetModel planetModel = randomPlanetModel();
int iters = atLeast(1000);
for(int iter=0;iter<iters;iter++) {
final double latBase = GeoTestUtil.nextLatitude();
final double lonBase = GeoTestUtil.nextLongitude();
// test above the value
double lat = latBase;
double lon = lonBase;
for (int i = 0; i < 1000; i++) {
lat = Math.min(90, Math.nextUp(lat));
lon = Math.min(180, Math.nextUp(lon));
GeoPoint point = new GeoPoint(planetModel, toRadians(lat), toRadians(lon));
GeoPoint pointEnc = new GeoPoint(Geo3DUtil.decodeValueFloor(planetModel.encodeValue(point.x), planetModel),
Geo3DUtil.decodeValueFloor(planetModel.encodeValue(point.y), planetModel),
Geo3DUtil.decodeValueFloor(planetModel.encodeValue(point.z), planetModel));
assertTrue(pointEnc.x <= point.x);
assertTrue(pointEnc.y <= point.y);
assertTrue(pointEnc.z <= point.z);
}
// test below the value
lat = latBase;
lon = lonBase;
for (int i = 0; i < 1000; i++) {
lat = Math.max(-90, Math.nextDown(lat));
lon = Math.max(-180, Math.nextDown(lon));
GeoPoint point = new GeoPoint(planetModel, toRadians(lat), toRadians(lon));
GeoPoint pointEnc = new GeoPoint(Geo3DUtil.decodeValueFloor(planetModel.encodeValue(point.x), planetModel),
Geo3DUtil.decodeValueFloor(planetModel.encodeValue(point.y), planetModel),
Geo3DUtil.decodeValueFloor(planetModel.encodeValue(point.z), planetModel));
assertTrue(pointEnc.x <= point.x);
assertTrue(pointEnc.y <= point.y);
assertTrue(pointEnc.z <= point.z);
}
}
}
public void testMinValueQuantization(){
PlanetModel planetModel = randomPlanetModel();
int encoded = planetModel.MIN_ENCODED_VALUE;
double minValue= -planetModel.getMaximumMagnitude();
//Normal encoding
double decoded = planetModel.decodeValue(encoded);
assertEquals(minValue, decoded, 0d);
assertEquals(encoded, planetModel.encodeValue(decoded));
//Encoding floor
double decodedFloor = Geo3DUtil.decodeValueFloor(encoded, planetModel);
assertEquals(minValue, decodedFloor, 0d);
assertEquals(encoded, planetModel.encodeValue(decodedFloor));
//Encoding ceiling
double decodedCeiling = Geo3DUtil.decodeValueCeil(encoded, planetModel);
assertTrue(decodedCeiling > minValue);
assertEquals(encoded, planetModel.encodeValue(decodedCeiling));
}
public void testMaxValueQuantization(){
PlanetModel planetModel = randomPlanetModel();
int encoded = planetModel.MAX_ENCODED_VALUE;
double maxValue= planetModel.getMaximumMagnitude();
//Normal encoding
double decoded = planetModel.decodeValue(encoded);
assertEquals(maxValue, decoded, 0d);
assertEquals(encoded, planetModel.encodeValue(decoded));
//Encoding floor
double decodedFloor = Geo3DUtil.decodeValueFloor(encoded, planetModel);
assertTrue(decodedFloor < maxValue);
assertEquals(encoded, planetModel.encodeValue(decodedFloor));
//Encoding ceiling
double decodedCeiling = Geo3DUtil.decodeValueCeil(encoded, planetModel);
assertEquals(maxValue, decodedCeiling, 0d);
assertEquals(encoded, planetModel.encodeValue(decodedCeiling));
}
// poached from TestGeoEncodingUtils.testLatitudeQuantization:
/**
* step through some integers, ensuring they decode to their expected double values.
* double values start at -planetMax and increase by Geo3DUtil.DECODE for each integer.
* check edge cases within the double range and random doubles within the range too.
*/
public void testQuantization() throws Exception {
Random random = random();
PlanetModel planetModel = randomPlanetModel();
for (int i = 0; i < 10000; i++) {
int encoded = random.nextInt();
if (encoded <= planetModel.MIN_ENCODED_VALUE) {
continue;
}
if (encoded >= planetModel.MAX_ENCODED_VALUE) {
continue;
}
double min = encoded * planetModel.DECODE;
double decoded = Geo3DUtil.decodeValueFloor(encoded, planetModel);
// should exactly equal expected value
assertEquals(min, decoded, 0.0D);
// should round-trip
assertEquals(encoded, planetModel.encodeValue(decoded));
// test within the range
if (encoded != Integer.MAX_VALUE) {
// this is the next representable value
// all double values between [min .. max) should encode to the current integer
double max = min + planetModel.DECODE;
assertEquals(max, Geo3DUtil.decodeValueFloor(encoded+1, planetModel), 0.0D);
assertEquals(encoded+1, planetModel.encodeValue(max));
// first and last doubles in range that will be quantized
double minEdge = Math.nextUp(min);
double maxEdge = Math.nextDown(max);
assertEquals(encoded, planetModel.encodeValue(minEdge));
assertEquals(encoded, planetModel.encodeValue(maxEdge));
// check random values within the double range
long minBits = NumericUtils.doubleToSortableLong(minEdge);
long maxBits = NumericUtils.doubleToSortableLong(maxEdge);
for (int j = 0; j < 100; j++) {
double value = NumericUtils.sortableLongToDouble(TestUtil.nextLong(random, minBits, maxBits));
// round down
assertEquals(encoded, planetModel.encodeValue(value));
}
}
}
}
public void testEncodeDecodeIsStable() throws Exception {
PlanetModel planetModel = randomPlanetModel();
int iters = atLeast(1000);
for(int iter=0;iter<iters;iter++) {
double lat = GeoTestUtil.nextLatitude();
double lon = GeoTestUtil.nextLongitude();
GeoPoint point = new GeoPoint(planetModel, toRadians(lat), toRadians(lon));
// encode point
GeoPoint pointEnc = new GeoPoint(planetModel.decodeValue(planetModel.encodeValue(point.x)),
planetModel.decodeValue(planetModel.encodeValue(point.y)),
planetModel.decodeValue(planetModel.encodeValue(point.z)));
// encode it again (double encode)
GeoPoint pointEnc2 = new GeoPoint(planetModel.decodeValue(planetModel.encodeValue(pointEnc.x)),
planetModel.decodeValue(planetModel.encodeValue(pointEnc.y)),
planetModel.decodeValue(planetModel.encodeValue(pointEnc.z)));
//System.out.println("TEST " + iter + ":\n point =" + point + "\n pointEnc =" + pointEnc + "\n pointEnc2=" + pointEnc2);
assertEquals(pointEnc.x, pointEnc2.x, 0.0);
assertEquals(pointEnc.y, pointEnc2.y, 0.0);
assertEquals(pointEnc.z, pointEnc2.z, 0.0);
}
}
public void testDistanceQuery() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter w = new RandomIndexWriter(random(), dir);
PlanetModel planetModel = randomPlanetModel();
// index two points:
Document doc = new Document();
doc.add(new Geo3DPoint("field", planetModel, 0.1 , 0.1));
w.addDocument(doc);
doc = new Document();
doc.add(new Geo3DPoint("field", planetModel, 90 , 1));
w.addDocument(doc);
///// search //////
IndexReader reader = w.getReader();
w.close();
IndexSearcher searcher = newSearcher(reader);
// simple query, in any planet model one point should be inside and the other outside
Query q = Geo3DPoint.newDistanceQuery("field", planetModel, 0, 0, planetModel.getMeanRadius() * 0.01);
assertEquals(1, searcher.count(q));
IOUtils.close(w, reader, dir);
}
/** Clips the incoming value to the allowed min/max range before encoding, instead of throwing an exception. */
private static int encodeValueLenient(double x, PlanetModel planetModel) {
double planetMax = planetModel.getMaximumMagnitude();
if (x > planetMax) {
x = planetMax;
} else if (x < -planetMax) {
x = -planetMax;
}
return planetModel.encodeValue(x);
}
private static class ExplainingVisitor implements IntersectVisitor {
final GeoShape shape;
final GeoPoint targetDocPoint;
final GeoPoint scaledDocPoint;
final IntersectVisitor in;
final List<Cell> stack = new ArrayList<>();
private List<Cell> stackToTargetDoc;
final int targetDocID;
final int numDims;
final int bytesPerDim;
private int targetStackUpto;
final StringBuilder b;
// In the first phase, we always return CROSSES to do a full scan of the BKD tree to see which leaf block the document lives in
boolean firstPhase = true;
public ExplainingVisitor(GeoShape shape, GeoPoint targetDocPoint, GeoPoint scaledDocPoint, IntersectVisitor in, int targetDocID, int numDims, int bytesPerDim, StringBuilder b) {
this.shape = shape;
this.targetDocPoint = targetDocPoint;
this.scaledDocPoint = scaledDocPoint;
this.in = in;
this.targetDocID = targetDocID;
this.numDims = numDims;
this.bytesPerDim = bytesPerDim;
this.b = b;
}
public void startSecondPhase() {
if (firstPhase == false) {
throw new IllegalStateException("already started second phase");
}
if (stackToTargetDoc == null) {
b.append("target docID=" + targetDocID + " was never seen in points!\n");
}
firstPhase = false;
stack.clear();
}
@Override
public void visit(int docID) throws IOException {
assert firstPhase == false;
if (docID == targetDocID) {
b.append("leaf visit docID=" + docID + "\n");
}
}
@Override
public void visit(int docID, byte[] packedValue) throws IOException {
if (firstPhase) {
if (docID == targetDocID) {
assert stackToTargetDoc == null;
stackToTargetDoc = new ArrayList<>(stack);
b.append(" full BKD path to target doc:\n");
for(Cell cell : stack) {
b.append(" " + cell + "\n");
}
}
} else {
if (docID == targetDocID) {
double x = Geo3DPoint.decodeDimension(packedValue, 0, shape.getPlanetModel());
double y = Geo3DPoint.decodeDimension(packedValue, Integer.BYTES, shape.getPlanetModel());
double z = Geo3DPoint.decodeDimension(packedValue, 2 * Integer.BYTES, shape.getPlanetModel());
b.append("leaf visit docID=" + docID + " x=" + x + " y=" + y + " z=" + z + "\n");
in.visit(docID, packedValue);
}
}
}
@Override
public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
Cell cell = new Cell(minPackedValue, maxPackedValue);
//System.out.println("compare: " + cell);
// TODO: this is a bit hacky, having to reverse-engineer where we are in the BKD tree's recursion ... but it's the lesser evil vs e.g.
// polluting this visitor API, or implementing this "under the hood" in BKDReader instead?
if (firstPhase) {
// Pop stack:
while (stack.size() > 0 && stack.get(stack.size()-1).contains(cell) == false) {
stack.remove(stack.size()-1);
//System.out.println(" pop");
}
// Push stack:
stack.add(cell);
//System.out.println(" push");
return Relation.CELL_CROSSES_QUERY;
} else {
Relation result = in.compare(minPackedValue, maxPackedValue);
if (targetStackUpto < stackToTargetDoc.size() && cell.equals(stackToTargetDoc.get(targetStackUpto))) {
b.append(" on cell " + stackToTargetDoc.get(targetStackUpto) + ", wrapped visitor returned " + result + "\n");
targetStackUpto++;
}
return result;
}
}
private class Cell {
private final byte[] minPackedValue;
private final byte[] maxPackedValue;
public Cell(byte[] minPackedValue, byte[] maxPackedValue) {
this.minPackedValue = minPackedValue.clone();
this.maxPackedValue = maxPackedValue.clone();
}
/** Returns true if this cell fully contains the other one */
public boolean contains(Cell other) {
for(int dim=0;dim<numDims;dim++) {
int offset = bytesPerDim * dim;
// other.min < this.min?
if (FutureArrays.compareUnsigned(other.minPackedValue, offset, offset + bytesPerDim, minPackedValue, offset, offset + bytesPerDim) < 0) {
return false;
}
// other.max < this.max?
if (FutureArrays.compareUnsigned(other.maxPackedValue, offset, offset + bytesPerDim, maxPackedValue, offset, offset + bytesPerDim) > 0) {
return false;
}
}
return true;
}
@Override
public String toString() {
double xMin = Geo3DUtil.decodeValueFloor(NumericUtils.sortableBytesToInt(minPackedValue, 0), shape.getPlanetModel());
double xMax = Geo3DUtil.decodeValueCeil(NumericUtils.sortableBytesToInt(maxPackedValue, 0), shape.getPlanetModel());
double yMin = Geo3DUtil.decodeValueFloor(NumericUtils.sortableBytesToInt(minPackedValue, 1 * Integer.BYTES), shape.getPlanetModel());
double yMax = Geo3DUtil.decodeValueCeil(NumericUtils.sortableBytesToInt(maxPackedValue, 1 * Integer.BYTES), shape.getPlanetModel());
double zMin = Geo3DUtil.decodeValueFloor(NumericUtils.sortableBytesToInt(minPackedValue, 2 * Integer.BYTES), shape.getPlanetModel());
double zMax = Geo3DUtil.decodeValueCeil(NumericUtils.sortableBytesToInt(maxPackedValue, 2 * Integer.BYTES), shape.getPlanetModel());
final XYZSolid xyzSolid = XYZSolidFactory.makeXYZSolid(shape.getPlanetModel(), xMin, xMax, yMin, yMax, zMin, zMax);
final int relationship = xyzSolid.getRelationship(shape);
final boolean pointWithinCell = xyzSolid.isWithin(targetDocPoint);
final boolean scaledWithinCell = xyzSolid.isWithin(scaledDocPoint);
final String relationshipString;
switch (relationship) {
case GeoArea.CONTAINS:
relationshipString = "CONTAINS";
break;
case GeoArea.WITHIN:
relationshipString = "WITHIN";
break;
case GeoArea.OVERLAPS:
relationshipString = "OVERLAPS";
break;
case GeoArea.DISJOINT:
relationshipString = "DISJOINT";
break;
default:
relationshipString = "UNKNOWN";
break;
}
return "Cell(x=" + xMin + " TO " + xMax + " y=" + yMin + " TO " + yMax + " z=" + zMin + " TO " + zMax +
"); Shape relationship = "+relationshipString+
"; Quantized point within cell = "+pointWithinCell+
"; Unquantized point within cell = "+scaledWithinCell;
}
@Override
public boolean equals(Object other) {
if (other instanceof Cell == false) {
return false;
}
Cell otherCell = (Cell) other;
return Arrays.equals(minPackedValue, otherCell.minPackedValue) && Arrays.equals(maxPackedValue, otherCell.maxPackedValue);
}
@Override
public int hashCode() {
return Arrays.hashCode(minPackedValue) + Arrays.hashCode(maxPackedValue);
}
}
}
public static String explain(String fieldName, GeoShape shape, GeoPoint targetDocPoint, GeoPoint scaledDocPoint, IndexReader reader, int docID) throws Exception {
final XYZBounds bounds = new XYZBounds();
shape.getBounds(bounds);
// First find the leaf reader that owns this doc:
int subIndex = ReaderUtil.subIndex(docID, reader.leaves());
LeafReader leafReader = reader.leaves().get(subIndex).reader();
StringBuilder b = new StringBuilder();
b.append("target is in leaf " + leafReader + " of full reader " + reader + "\n");
DocIdSetBuilder hits = new DocIdSetBuilder(leafReader.maxDoc());
ExplainingVisitor visitor = new ExplainingVisitor(shape, targetDocPoint, scaledDocPoint,
new PointInShapeIntersectVisitor(hits, shape, bounds),
docID - reader.leaves().get(subIndex).docBase, 3, Integer.BYTES, b);
// Do first phase, where we just figure out the "path" that leads to the target docID:
leafReader.getPointValues(fieldName).intersect(visitor);
// Do second phase, where we we see how the wrapped visitor responded along that path:
visitor.startSecondPhase();
leafReader.getPointValues(fieldName).intersect(visitor);
return b.toString();
}
}