package org.apache.lucene.spatial.prefix;
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.distance.DistanceUtils;
import com.spatial4j.core.io.GeohashUtils;
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.Rectangle;
import com.spatial4j.core.shape.Shape;
import org.apache.lucene.spatial.SpatialMatchConcern;
import org.apache.lucene.spatial.StrategyTestCase;
import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.junit.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class TestRecursivePrefixTreeStrategy extends StrategyTestCase {
private int maxLength;
//Tests should call this first.
private void init(int maxLength) {
this.maxLength = maxLength;
this.ctx = SpatialContext.GEO;
GeohashPrefixTree grid = new GeohashPrefixTree(ctx, maxLength);
this.strategy = new RecursivePrefixTreeStrategy(grid, getClass().getSimpleName());
}
@Test
public void testFilterWithVariableScanLevel() throws IOException {
init(GeohashPrefixTree.getMaxLevelsPossible());
getAddAndVerifyIndexedDocuments(DATA_WORLD_CITIES_POINTS);
//execute queries for each prefix grid scan level
for(int i = 0; i <= maxLength; i++) {
((RecursivePrefixTreeStrategy)strategy).setPrefixGridScanLevel(i);
executeQueries(SpatialMatchConcern.FILTER, QTEST_Cities_Intersects_BBox);
}
}
@Test
public void testOneMeterPrecision() {
init(GeohashPrefixTree.getMaxLevelsPossible());
GeohashPrefixTree grid = (GeohashPrefixTree) ((RecursivePrefixTreeStrategy) strategy).getGrid();
//DWS: I know this to be true. 11 is needed for one meter
double degrees = DistanceUtils.dist2Degrees(0.001, DistanceUtils.EARTH_MEAN_RADIUS_KM);
assertEquals(11, grid.getLevelForDistance(degrees));
}
@Test
public void testPrecision() throws IOException{
init(GeohashPrefixTree.getMaxLevelsPossible());
Point iPt = ctx.makePoint(2.8028712999999925, 48.3708044);//lon, lat
addDocument(newDoc("iPt", iPt));
commit();
Point qPt = ctx.makePoint(2.4632387000000335, 48.6003516);
final double KM2DEG = DistanceUtils.dist2Degrees(1, DistanceUtils.EARTH_MEAN_RADIUS_KM);
final double DEG2KM = 1 / KM2DEG;
final double DIST = 35.75;//35.7499...
assertEquals(DIST, ctx.getDistCalc().distance(iPt, qPt) * DEG2KM, 0.001);
//distErrPct will affect the query shape precision. The indexed precision
// was set to nearly zilch via init(GeohashPrefixTree.getMaxLevelsPossible());
final double distErrPct = 0.025; //the suggested default, by the way
final double distMult = 1+distErrPct;
assertTrue(35.74*distMult >= DIST);
checkHits(q(qPt, 35.74 * KM2DEG, distErrPct), 1, null);
assertTrue(30*distMult < DIST);
checkHits(q(qPt, 30 * KM2DEG, distErrPct), 0, null);
assertTrue(33*distMult < DIST);
checkHits(q(qPt, 33 * KM2DEG, distErrPct), 0, null);
assertTrue(34*distMult < DIST);
checkHits(q(qPt, 34 * KM2DEG, distErrPct), 0, null);
}
@Test
public void geohashRecursiveRandom() throws IOException {
init(12);
//1. Iterate test with the cluster at some worldly point of interest
Point[] clusterCenters = new Point[]{ctx.makePoint(-180,0), ctx.makePoint(0,90), ctx.makePoint(0,-90)};
for (Point clusterCenter : clusterCenters) {
//2. Iterate on size of cluster (a really small one and a large one)
String hashCenter = GeohashUtils.encodeLatLon(clusterCenter.getY(), clusterCenter.getX(), maxLength);
//calculate the number of degrees in the smallest grid box size (use for both lat & lon)
String smallBox = hashCenter.substring(0,hashCenter.length()-1);//chop off leaf precision
Rectangle clusterDims = GeohashUtils.decodeBoundary(smallBox,ctx);
double smallRadius = Math.max(clusterDims.getMaxX()-clusterDims.getMinX(),clusterDims.getMaxY()-clusterDims.getMinY());
assert smallRadius < 1;
double largeRadius = 20d;//good large size; don't use >=45 for this test code to work
double[] radiusDegs = {largeRadius,smallRadius};
for (double radiusDeg : radiusDegs) {
//3. Index random points in this cluster circle
deleteAll();
List<Point> points = new ArrayList<Point>();
for(int i = 0; i < 20; i++) {
//Note that this will not result in randomly distributed points in the
// circle, they will be concentrated towards the center a little. But
// it's good enough.
Point pt = ctx.getDistCalc().pointOnBearing(clusterCenter,
random().nextDouble() * radiusDeg, random().nextInt() * 360, ctx, null);
pt = alignGeohash(pt);
points.add(pt);
addDocument(newDoc("" + i, pt));
}
commit();
//3. Use some query centers. Each is twice the cluster's radius away.
for(int ri = 0; ri < 4; ri++) {
Point queryCenter = ctx.getDistCalc().pointOnBearing(clusterCenter,
radiusDeg*2, random().nextInt(360), ctx, null);
queryCenter = alignGeohash(queryCenter);
//4.1 Query a small box getting nothing
checkHits(q(queryCenter, radiusDeg - smallRadius/2), 0, null);
//4.2 Query a large box enclosing the cluster, getting everything
checkHits(q(queryCenter, radiusDeg*3 + smallRadius/2), points.size(), null);
//4.3 Query a medium box getting some (calculate the correct solution and verify)
double queryDist = radiusDeg * 2;
//Find matching points. Put into int[] of doc ids which is the same thing as the index into points list.
int[] ids = new int[points.size()];
int ids_sz = 0;
for (int i = 0; i < points.size(); i++) {
Point point = points.get(i);
if (ctx.getDistCalc().distance(queryCenter, point) <= queryDist)
ids[ids_sz++] = i;
}
ids = Arrays.copyOf(ids, ids_sz);
//assert ids_sz > 0 (can't because randomness keeps us from being able to)
checkHits(q(queryCenter, queryDist), ids.length, ids);
}
}//for radiusDeg
}//for clusterCenter
}//randomTest()
/** Query point-distance (in degrees) with zero error percent. */
private SpatialArgs q(Point pt, double distDEG) {
return q(pt, distDEG, 0.0);
}
private SpatialArgs q(Point pt, double distDEG, double distErrPct) {
Shape shape = ctx.makeCircle(pt, distDEG);
SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects,shape);
args.setDistErrPct(distErrPct);
return args;
}
private void checkHits(SpatialArgs args, int assertNumFound, int[] assertIds) {
SearchResults got = executeQuery(strategy.makeQuery(args), 100);
assertEquals("" + args, assertNumFound, got.numFound);
if (assertIds != null) {
Set<Integer> gotIds = new HashSet<Integer>();
for (SearchResult result : got.results) {
gotIds.add(Integer.valueOf(result.document.get("id")));
}
for (int assertId : assertIds) {
assertTrue("has "+assertId,gotIds.contains(assertId));
}
}
}
/** NGeohash round-trip for given precision. */
private Point alignGeohash(Point p) {
return GeohashUtils.decode(GeohashUtils.encodeLatLon(p.getY(), p.getX(), maxLength), ctx);
}
}