You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by nk...@apache.org on 2016/02/05 21:41:26 UTC
[4/7] lucene-solr git commit: LUCENE-6997: refactor sandboxed
GeoPointField and query classes to lucene-spatial module
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/187fe876/lucene/sandbox/src/test/org/apache/lucene/util/BaseGeoPointTestCase.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/util/BaseGeoPointTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/util/BaseGeoPointTestCase.java
deleted file mode 100644
index 4b4f8d7..0000000
--- a/lucene/sandbox/src/test/org/apache/lucene/util/BaseGeoPointTestCase.java
+++ /dev/null
@@ -1,755 +0,0 @@
-/*
- * 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.util;
-
-import java.io.IOException;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.apache.lucene.document.Document;
-import org.apache.lucene.document.Field;
-import org.apache.lucene.document.NumericDocValuesField;
-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.RandomIndexWriter;
-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.junit.BeforeClass;
-
-// TODO: cutover TestGeoUtils too?
-
-public abstract class BaseGeoPointTestCase extends LuceneTestCase {
-
- protected static final String FIELD_NAME = "point";
-
- private static final double LON_SCALE = (0x1L<<GeoUtils.BITS)/360.0D;
- private static final double LAT_SCALE = (0x1L<<GeoUtils.BITS)/180.0D;
-
- private static double originLat;
- private static double originLon;
- private static double lonRange;
- private static double latRange;
-
- @BeforeClass
- public static void beforeClassBase() throws Exception {
- // Between 1.0 and 3.0:
- lonRange = 2 * (random().nextDouble() + 0.5);
- latRange = 2 * (random().nextDouble() + 0.5);
-
- originLon = GeoUtils.normalizeLon(GeoUtils.MIN_LON_INCL + lonRange + (GeoUtils.MAX_LON_INCL - GeoUtils.MIN_LON_INCL - 2 * lonRange) * random().nextDouble());
- originLat = GeoUtils.normalizeLat(GeoUtils.MIN_LAT_INCL + latRange + (GeoUtils.MAX_LAT_INCL - GeoUtils.MIN_LAT_INCL - 2 * latRange) * random().nextDouble());
- }
-
- // A particularly tricky adversary for BKD tree:
- @Nightly
- public void testSamePointManyTimes() throws Exception {
- int numPoints = atLeast(1000);
- // TODO: GeoUtils are potentially slow if we use small=false with heavy testing
- boolean small = random().nextBoolean();
-
- // Every doc has 2 points:
- double theLat = randomLat(small);
- double theLon = randomLon(small);
-
- double[] lats = new double[numPoints];
- Arrays.fill(lats, theLat);
-
- double[] lons = new double[numPoints];
- Arrays.fill(lons, theLon);
-
- verify(small, lats, lons);
- }
-
- @Nightly
- public void testAllLatEqual() throws Exception {
- int numPoints = atLeast(10000);
- // TODO: GeoUtils are potentially slow if we use small=false with heavy testing
- // boolean small = random().nextBoolean();
- boolean small = true;
- double lat = randomLat(small);
- 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.out.println(" doc=" + docID + " is missing");
- }
- continue;
- }
-
- if (docID > 0 && x == 14 && haveRealDoc) {
- int oldDocID;
- while (true) {
- oldDocID = random().nextInt(docID);
- if (Double.isNaN(lats[oldDocID]) == false) {
- break;
- }
- }
-
- // Fully identical point:
- lons[docID] = lons[oldDocID];
- if (VERBOSE) {
- System.out.println(" doc=" + docID + " lat=" + lat + " lon=" + lons[docID] + " (same lat/lon as doc=" + oldDocID + ")");
- }
- } else {
- lons[docID] = randomLon(small);
- haveRealDoc = true;
- if (VERBOSE) {
- System.out.println(" doc=" + docID + " lat=" + lat + " lon=" + lons[docID]);
- }
- }
- lats[docID] = lat;
- }
-
- verify(small, lats, lons);
- }
-
- @Nightly
- public void testAllLonEqual() throws Exception {
- int numPoints = atLeast(10000);
- // TODO: GeoUtils are potentially slow if we use small=false with heavy testing
- // boolean small = random().nextBoolean();
- boolean small = true;
- double theLon = randomLon(small);
- double[] lats = new double[numPoints];
- double[] lons = new double[numPoints];
-
- boolean haveRealDoc = false;
-
- //System.out.println("theLon=" + theLon);
-
- 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.out.println(" doc=" + docID + " is missing");
- }
- continue;
- }
-
- if (docID > 0 && x == 14 && haveRealDoc) {
- int oldDocID;
- while (true) {
- oldDocID = random().nextInt(docID);
- if (Double.isNaN(lats[oldDocID]) == false) {
- break;
- }
- }
-
- // Fully identical point:
- lats[docID] = lats[oldDocID];
- if (VERBOSE) {
- System.out.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + theLon + " (same lat/lon as doc=" + oldDocID + ")");
- }
- } else {
- lats[docID] = randomLat(small);
- haveRealDoc = true;
- if (VERBOSE) {
- System.out.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + theLon);
- }
- }
- lons[docID] = theLon;
- }
-
- verify(small, lats, lons);
- }
-
- @Nightly
- public void testMultiValued() throws Exception {
- int numPoints = atLeast(10000);
- // Every doc has 2 points:
- double[] lats = new double[2*numPoints];
- double[] lons = new double[2*numPoints];
- Directory dir = newDirectory();
- IndexWriterConfig iwc = newIndexWriterConfig();
- initIndexWriterConfig(FIELD_NAME, iwc);
-
- // We rely on docID order:
- iwc.setMergePolicy(newLogMergePolicy());
- RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc);
-
- // TODO: GeoUtils are potentially slow if we use small=false with heavy testing
- boolean small = random().nextBoolean();
- //boolean small = true;
-
- for (int id=0;id<numPoints;id++) {
- Document doc = new Document();
- lats[2*id] = randomLat(small);
- lons[2*id] = randomLon(small);
- doc.add(newStringField("id", ""+id, Field.Store.YES));
- addPointToDoc(FIELD_NAME, doc, lats[2*id], lons[2*id]);
- lats[2*id+1] = randomLat(small);
- lons[2*id+1] = randomLon(small);
- addPointToDoc(FIELD_NAME, doc, lats[2*id+1], lons[2*id+1]);
-
- if (VERBOSE) {
- System.out.println("id=" + id);
- System.out.println(" lat=" + lats[2*id] + " lon=" + lons[2*id]);
- System.out.println(" lat=" + lats[2*id+1] + " lon=" + lons[2*id+1]);
- }
- w.addDocument(doc);
- }
-
- if (random().nextBoolean()) {
- w.forceMerge(1);
- }
- IndexReader r = w.getReader();
- w.close();
-
- // We can't wrap with "exotic" readers because the BKD query must see the BKDDVFormat:
- IndexSearcher s = newSearcher(r, false);
-
- int iters = atLeast(75);
- for (int iter=0;iter<iters;iter++) {
- GeoRect rect = randomRect(small, small == false);
-
- if (VERBOSE) {
- System.out.println("\nTEST: iter=" + iter + " bbox=" + rect);
- }
-
- Query query = newBBoxQuery(FIELD_NAME, rect);
-
- 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);
- }
- });
-
- boolean fail = false;
-
- for(int docID=0;docID<lats.length/2;docID++) {
- double latDoc1 = lats[2*docID];
- double lonDoc1 = lons[2*docID];
- double latDoc2 = lats[2*docID+1];
- double lonDoc2 = lons[2*docID+1];
-
- Boolean result1 = rectContainsPoint(rect, latDoc1, lonDoc1);
- if (result1 == null) {
- // borderline case: cannot test
- continue;
- }
-
- Boolean result2 = rectContainsPoint(rect, latDoc2, lonDoc2);
- if (result2 == null) {
- // borderline case: cannot test
- continue;
- }
-
- boolean expected = result1 == Boolean.TRUE || result2 == Boolean.TRUE;
-
- if (hits.get(docID) != expected) {
- String id = s.doc(docID).get("id");
- if (expected) {
- System.out.println(Thread.currentThread().getName() + ": id=" + id + " docID=" + docID + " should match but did not");
- } else {
- System.out.println(Thread.currentThread().getName() + ": id=" + id + " docID=" + docID + " should not match but did");
- }
- System.out.println(" rect=" + rect);
- System.out.println(" lat=" + latDoc1 + " lon=" + lonDoc1 + "\n lat=" + latDoc2 + " lon=" + lonDoc2);
- System.out.println(" result1=" + result1 + " result2=" + result2);
- fail = true;
- }
- }
-
- if (fail) {
- fail("some hits were wrong");
- }
- }
- r.close();
- dir.close();
- }
-
- 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 {
- assumeFalse("Direct codec can OOME on this test", TestUtil.getDocValuesFormat(FIELD_NAME).equals("Direct"));
- assumeFalse("Memory codec can OOME on this test", TestUtil.getDocValuesFormat(FIELD_NAME).equals("Memory"));
- doTestRandom(200000);
- }
-
- private void doTestRandom(int count) throws Exception {
-
- int numPoints = atLeast(count);
-
- if (VERBOSE) {
- System.out.println("TEST: numPoints=" + numPoints);
- }
-
- double[] lats = new double[numPoints];
- double[] lons = new double[numPoints];
-
- // TODO: GeoUtils are potentially slow if we use small=false with heavy testing
- boolean small = random().nextBoolean();
-
- boolean haveRealDoc = false;
-
- for (int id=0;id<numPoints;id++) {
- int x = random().nextInt(20);
- if (x == 17) {
- // Some docs don't have a point:
- lats[id] = Double.NaN;
- if (VERBOSE) {
- System.out.println(" id=" + id + " is missing");
- }
- continue;
- }
-
- if (id > 0 && x < 3 && haveRealDoc) {
- int oldID;
- while (true) {
- oldID = random().nextInt(id);
- if (Double.isNaN(lats[oldID]) == false) {
- break;
- }
- }
-
- if (x == 0) {
- // Identical lat to old point
- lats[id] = lats[oldID];
- lons[id] = randomLon(small);
- if (VERBOSE) {
- System.out.println(" id=" + id + " lat=" + lats[id] + " lon=" + lons[id] + " (same lat as doc=" + oldID + ")");
- }
- } else if (x == 1) {
- // Identical lon to old point
- lats[id] = randomLat(small);
- lons[id] = lons[oldID];
- if (VERBOSE) {
- System.out.println(" id=" + id + " lat=" + lats[id] + " lon=" + lons[id] + " (same lon as doc=" + oldID + ")");
- }
- } else {
- assert x == 2;
- // Fully identical point:
- lats[id] = lats[oldID];
- lons[id] = lons[oldID];
- if (VERBOSE) {
- System.out.println(" id=" + id + " lat=" + lats[id] + " lon=" + lons[id] + " (same lat/lon as doc=" + oldID + ")");
- }
- }
- } else {
- lats[id] = randomLat(small);
- lons[id] = randomLon(small);
- haveRealDoc = true;
- if (VERBOSE) {
- System.out.println(" id=" + id + " lat=" + lats[id] + " lon=" + lons[id]);
- }
- }
- }
-
- verify(small, lats, lons);
- }
-
- public double randomLat(boolean small) {
- double result;
- if (small) {
- result = GeoUtils.normalizeLat(originLat + latRange * (random().nextDouble() - 0.5));
- } else {
- result = -90 + 180.0 * random().nextDouble();
- }
- return result;
- }
-
- public double randomLon(boolean small) {
- double result;
- if (small) {
- result = GeoUtils.normalizeLon(originLon + lonRange * (random().nextDouble() - 0.5));
- } else {
- result = -180 + 360.0 * random().nextDouble();
- }
- return result;
- }
-
- protected GeoRect randomRect(boolean small, boolean canCrossDateLine) {
- double lat0 = randomLat(small);
- double lat1 = randomLat(small);
- double lon0 = randomLon(small);
- double lon1 = randomLon(small);
-
- if (lat1 < lat0) {
- double x = lat0;
- lat0 = lat1;
- lat1 = x;
- }
-
- if (lat0 == lat1) {
- lat1 = randomLat(small);
- }
-
- if (lon0 == lon1) {
- lon1 = randomLon(small);
- }
-
- if (canCrossDateLine == false && lon1 < lon0) {
- double x = lon0;
- lon0 = lon1;
- lon1 = x;
- }
-
- return new GeoRect(lon0, lon1, lat0, lat1);
- }
-
- protected void initIndexWriterConfig(String field, IndexWriterConfig iwc) {
- }
-
- protected abstract void addPointToDoc(String field, Document doc, double lat, double lon);
-
- protected abstract Query newBBoxQuery(String field, GeoRect bbox);
-
- protected abstract Query newDistanceQuery(String field, double centerLat, double centerLon, double radiusMeters);
-
- protected abstract Query newDistanceRangeQuery(String field, double centerLat, double centerLon, double minRadiusMeters, double radiusMeters);
-
- protected abstract Query newPolygonQuery(String field, double[] lats, double[] lons);
-
- /** Returns null if it's borderline case */
- protected abstract Boolean rectContainsPoint(GeoRect rect, double pointLat, double pointLon);
-
- /** Returns null if it's borderline case */
- protected abstract Boolean polyRectContainsPoint(GeoRect rect, double pointLat, double pointLon);
-
- /** Returns null if it's borderline case */
- protected abstract Boolean circleContainsPoint(double centerLat, double centerLon, double radiusMeters, double pointLat, double pointLon);
-
- protected abstract Boolean distanceRangeContainsPoint(double centerLat, double centerLon, double minRadiusMeters, double radiusMeters, double pointLat, double pointLon);
-
- private static abstract class VerifyHits {
-
- public void test(AtomicBoolean failed, boolean small, IndexSearcher s, NumericDocValues docIDToID, Set<Integer> deleted, Query query, double[] lats, double[] lons) throws Exception {
- int maxDoc = s.getIndexReader().maxDoc();
- final FixedBitSet hits = new FixedBitSet(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);
- }
- });
-
- boolean fail = false;
-
- for(int docID=0;docID<maxDoc;docID++) {
- int id = (int) docIDToID.get(docID);
- Boolean expected;
- if (deleted.contains(id)) {
- expected = false;
- } else if (Double.isNaN(lats[id])) {
- expected = false;
- } else {
- expected = shouldMatch(lats[id], lons[id]);
- }
-
- // null means it's a borderline case which is allowed to be wrong:
- if (expected != null && hits.get(docID) != expected) {
- if (expected) {
- System.out.println(Thread.currentThread().getName() + ": id=" + id + " should match but did not");
- } else {
- System.out.println(Thread.currentThread().getName() + ": id=" + id + " should not match but did");
- }
- System.out.println(" small=" + small + " query=" + query +
- " docID=" + docID + "\n lat=" + lats[id] + " lon=" + lons[id] +
- "\n deleted?=" + deleted.contains(id));
- if (Double.isNaN(lats[id]) == false) {
- describe(docID, lats[id], lons[id]);
- }
- fail = true;
- }
- }
-
- if (fail) {
- failed.set(true);
- fail("some hits were wrong");
- }
- }
-
- /** Return true if we definitely should match, false if we definitely
- * should not match, and null if it's a borderline case which might
- * go either way. */
- protected abstract Boolean shouldMatch(double lat, double lon);
-
- protected abstract void describe(int docID, double lat, double lon);
- }
-
- protected void verify(final boolean small, final double[] lats, final 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);
- }
- initIndexWriterConfig(FIELD_NAME, iwc);
- Directory dir;
- if (lats.length > 100000) {
- dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
- } else {
- dir = newDirectory();
- }
-
- final 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) {
- addPointToDoc(FIELD_NAME, doc, 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.out.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 BKD query must see the BKDDVFormat:
- final IndexSearcher s = newSearcher(r, false);
-
- // Make sure queries are thread safe:
- int numThreads = TestUtil.nextInt(random(), 2, 5);
-
- List<Thread> threads = new ArrayList<>();
- final int iters = atLeast(75);
-
- 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++) {
-
- if (VERBOSE) {
- System.out.println("\nTEST: iter=" + iter + " s=" + s);
- }
- Query query;
- VerifyHits verifyHits;
-
- if (random().nextBoolean()) {
- // BBox: don't allow dateline crossing when testing small:
- final GeoRect bbox = randomRect(small, small == false);
-
- query = newBBoxQuery(FIELD_NAME, bbox);
-
- verifyHits = new VerifyHits() {
- @Override
- protected Boolean shouldMatch(double pointLat, double pointLon) {
- return rectContainsPoint(bbox, pointLat, pointLon);
- }
- @Override
- protected void describe(int docID, double lat, double lon) {
- }
- };
-
- } else if (random().nextBoolean()) {
- // Distance
- final boolean rangeQuery = random().nextBoolean();
- final double centerLat = randomLat(small);
- final double centerLon = randomLon(small);
-
- final double radiusMeters;
- final double minRadiusMeters;
-
- if (small) {
- // Approx 3 degrees lon at the equator:
- radiusMeters = random().nextDouble() * 333000 + 1.0;
- } else {
- // So the query can cover at most 50% of the earth's surface:
- radiusMeters = random().nextDouble() * GeoProjectionUtils.SEMIMAJOR_AXIS * Math.PI / 2.0 + 1.0;
- }
-
- // generate a random minimum radius between 1% and 95% the max radius
- minRadiusMeters = (0.01 + 0.94 * random().nextDouble()) * radiusMeters;
-
- if (VERBOSE) {
- final DecimalFormat df = new DecimalFormat("#,###.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
- System.out.println(" radiusMeters = " + df.format(radiusMeters)
- + ((rangeQuery == true) ? " minRadiusMeters = " + df.format(minRadiusMeters) : ""));
- }
-
- try {
- if (rangeQuery == true) {
- query = newDistanceRangeQuery(FIELD_NAME, centerLat, centerLon, minRadiusMeters, radiusMeters);
- } else {
- query = newDistanceQuery(FIELD_NAME, centerLat, centerLon, radiusMeters);
- }
- } catch (IllegalArgumentException e) {
- if (e.getMessage().contains("exceeds maxRadius")) {
- continue;
- }
- throw e;
- }
-
- verifyHits = new VerifyHits() {
- @Override
- protected Boolean shouldMatch(double pointLat, double pointLon) {
- final double radius = radiusMeters;
- final double minRadius = minRadiusMeters;
- if (rangeQuery == false) {
- return circleContainsPoint(centerLat, centerLon, radius, pointLat, pointLon);
- } else {
- return distanceRangeContainsPoint(centerLat, centerLon, minRadius, radius, pointLat, pointLon);
- }
- }
-
- @Override
- protected void describe(int docID, double pointLat, double pointLon) {
- double distanceKM = SloppyMath.haversin(centerLat, centerLon, pointLat, pointLon);
- System.out.println(" docID=" + docID + " centerLon=" + centerLon + " centerLat=" + centerLat
- + " pointLon=" + pointLon + " pointLat=" + pointLat + " distanceMeters=" + (distanceKM * 1000)
- + " vs" + ((rangeQuery == true) ? " minRadiusMeters=" + minRadiusMeters : "") + " radiusMeters=" + radiusMeters);
- }
- };
-
- // TODO: get poly query working with dateline crossing too (how?)!
- } else {
-
- // TODO: poly query can't handle dateline crossing yet:
- final GeoRect bbox = randomRect(small, false);
-
- // Polygon
- double[] lats = new double[5];
- double[] lons = new double[5];
- lats[0] = bbox.minLat;
- lons[0] = bbox.minLon;
- lats[1] = bbox.maxLat;
- lons[1] = bbox.minLon;
- lats[2] = bbox.maxLat;
- lons[2] = bbox.maxLon;
- lats[3] = bbox.minLat;
- lons[3] = bbox.maxLon;
- lats[4] = bbox.minLat;
- lons[4] = bbox.minLon;
- query = newPolygonQuery(FIELD_NAME, lats, lons);
-
- verifyHits = new VerifyHits() {
- @Override
- protected Boolean shouldMatch(double pointLat, double pointLon) {
- return polyRectContainsPoint(bbox, pointLat, pointLon);
- }
-
- @Override
- protected void describe(int docID, double lat, double lon) {
- }
- };
- }
-
- if (query != null) {
-
- if (VERBOSE) {
- System.out.println(" query=" + query);
- }
-
- verifyHits.test(failed, small, s, docIDToID, deleted, query, lats, lons);
- }
- }
- }
- };
- thread.setName("T" + i);
- thread.start();
- threads.add(thread);
- }
- startingGun.countDown();
- for(Thread thread : threads) {
- thread.join();
- }
- IOUtils.close(r, dir);
- assertFalse(failed.get());
- }
-}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/187fe876/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java b/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java
deleted file mode 100644
index fa662f9..0000000
--- a/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java
+++ /dev/null
@@ -1,545 +0,0 @@
-/*
- * 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.util;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import org.junit.BeforeClass;
-
-import com.carrotsearch.randomizedtesting.generators.RandomInts;
-
-import static org.apache.lucene.util.GeoDistanceUtils.DISTANCE_PCT_ERR;
-
-/**
- * Tests class for methods in GeoUtils
- *
- * @lucene.experimental
- */
-public class TestGeoUtils extends LuceneTestCase {
-
- private static final double LON_SCALE = (0x1L<<GeoUtils.BITS)/360.0D;
- private static final double LAT_SCALE = (0x1L<<GeoUtils.BITS)/180.0D;
-
- // Global bounding box we will "cover" in the random test; we have to make this "smallish" else the queries take very long:
- private static double originLat;
- private static double originLon;
- // private static double range;
- private static double lonRange;
- private static double latRange;
-
- @BeforeClass
- public static void beforeClass() throws Exception {
- // Between 1.0 and 3.0:
- lonRange = 2 * (random().nextDouble() + 0.5);
- latRange = 2 * (random().nextDouble() + 0.5);
-
- originLon = GeoUtils.MIN_LON_INCL + lonRange + (GeoUtils.MAX_LON_INCL - GeoUtils.MIN_LON_INCL - 2 * lonRange) * random().nextDouble();
- originLon = GeoUtils.normalizeLon(originLon);
- originLat = GeoUtils.MIN_LAT_INCL + latRange + (GeoUtils.MAX_LAT_INCL - GeoUtils.MIN_LAT_INCL - 2 * latRange) * random().nextDouble();
- originLat = GeoUtils.normalizeLat(originLat);
-
- if (VERBOSE) {
- System.out.println("TEST: originLon=" + originLon + " lonRange= " + lonRange + " originLat=" + originLat + " latRange=" + latRange);
- }
- }
-
- public void testGeoHash() {
- int numPoints = atLeast(100);
- String randomGeoHashString;
- String mortonGeoHash;
- long mortonLongFromGHLong, geoHashLong, mortonLongFromGHString;
- int randomLevel;
- for (int i = 0; i < numPoints; ++i) {
- // random point
- double lat = randomLat(false);
- double lon = randomLon(false);
-
- // compute geohash straight from lat/lon and from morton encoded value to ensure they're the same
- randomGeoHashString = GeoHashUtils.stringEncode(lon, lat, randomLevel = random().nextInt(12 - 1) + 1);
- mortonGeoHash = GeoHashUtils.stringEncodeFromMortonLong(GeoUtils.mortonHash(lon, lat), randomLevel);
- assertEquals(randomGeoHashString, mortonGeoHash);
-
- // v&v conversion from lat/lon or geohashstring to geohash long and back to geohash string
- geoHashLong = (random().nextBoolean()) ? GeoHashUtils.longEncode(lon, lat, randomLevel) : GeoHashUtils.longEncode(randomGeoHashString);
- assertEquals(randomGeoHashString, GeoHashUtils.stringEncode(geoHashLong));
-
- // v&v conversion from geohash long to morton long
- mortonLongFromGHString = GeoHashUtils.mortonEncode(randomGeoHashString);
- mortonLongFromGHLong = GeoHashUtils.mortonEncode(geoHashLong);
- assertEquals(mortonLongFromGHLong, mortonLongFromGHString);
-
- // v&v lat/lon from geohash string and geohash long
- assertEquals(GeoUtils.mortonUnhashLat(mortonLongFromGHString), GeoUtils.mortonUnhashLat(mortonLongFromGHLong), 0);
- assertEquals(GeoUtils.mortonUnhashLon(mortonLongFromGHString), GeoUtils.mortonUnhashLon(mortonLongFromGHLong), 0);
- }
- }
-
- /**
- * Pass condition: lat=42.6, lng=-5.6 should be encoded as "ezs42e44yx96",
- * lat=57.64911 lng=10.40744 should be encoded as "u4pruydqqvj8"
- */
- public void testEncode() {
- String hash = GeoHashUtils.stringEncode(-5.6, 42.6, 12);
- assertEquals("ezs42e44yx96", hash);
-
- hash = GeoHashUtils.stringEncode(10.40744, 57.64911, 12);
- assertEquals("u4pruydqqvj8", hash);
- }
-
- /**
- * Pass condition: lat=52.3738007, lng=4.8909347 should be encoded and then
- * decoded within 0.00001 of the original value
- */
- public void testDecodePreciseLongitudeLatitude() {
- final String geohash = GeoHashUtils.stringEncode(4.8909347, 52.3738007);
- final long hash = GeoHashUtils.mortonEncode(geohash);
-
- assertEquals(52.3738007, GeoUtils.mortonUnhashLat(hash), 0.00001D);
- assertEquals(4.8909347, GeoUtils.mortonUnhashLon(hash), 0.00001D);
- }
-
- /**
- * Pass condition: lat=84.6, lng=10.5 should be encoded and then decoded
- * within 0.00001 of the original value
- */
- public void testDecodeImpreciseLongitudeLatitude() {
- final String geohash = GeoHashUtils.stringEncode(10.5, 84.6);
-
- final long hash = GeoHashUtils.mortonEncode(geohash);
-
- assertEquals(84.6, GeoUtils.mortonUnhashLat(hash), 0.00001D);
- assertEquals(10.5, GeoUtils.mortonUnhashLon(hash), 0.00001D);
- }
-
- public void testDecodeEncode() {
- final String geoHash = "u173zq37x014";
- assertEquals(geoHash, GeoHashUtils.stringEncode(4.8909347, 52.3738007));
- final long mortonHash = GeoHashUtils.mortonEncode(geoHash);
- final double lon = GeoUtils.mortonUnhashLon(mortonHash);
- final double lat = GeoUtils.mortonUnhashLat(mortonHash);
- assertEquals(52.37380061d, GeoUtils.mortonUnhashLat(mortonHash), 0.000001d);
- assertEquals(4.8909343d, GeoUtils.mortonUnhashLon(mortonHash), 0.000001d);
-
- assertEquals(geoHash, GeoHashUtils.stringEncode(lon, lat));
- }
-
- public void testNeighbors() {
- String geohash = "gcpv";
- List<String> expectedNeighbors = new ArrayList<>();
- expectedNeighbors.add("gcpw");
- expectedNeighbors.add("gcpy");
- expectedNeighbors.add("u10n");
- expectedNeighbors.add("gcpt");
- expectedNeighbors.add("u10j");
- expectedNeighbors.add("gcps");
- expectedNeighbors.add("gcpu");
- expectedNeighbors.add("u10h");
- Collection<? super String> neighbors = new ArrayList<>();
- GeoHashUtils.addNeighbors(geohash, neighbors );
- assertEquals(expectedNeighbors, neighbors);
-
- // Border odd geohash
- geohash = "u09x";
- expectedNeighbors = new ArrayList<>();
- expectedNeighbors.add("u0c2");
- expectedNeighbors.add("u0c8");
- expectedNeighbors.add("u0cb");
- expectedNeighbors.add("u09r");
- expectedNeighbors.add("u09z");
- expectedNeighbors.add("u09q");
- expectedNeighbors.add("u09w");
- expectedNeighbors.add("u09y");
- neighbors = new ArrayList<>();
- GeoHashUtils.addNeighbors(geohash, neighbors);
- assertEquals(expectedNeighbors, neighbors);
-
- // Border even geohash
- geohash = "u09tv";
- expectedNeighbors = new ArrayList<>();
- expectedNeighbors.add("u09wh");
- expectedNeighbors.add("u09wj");
- expectedNeighbors.add("u09wn");
- expectedNeighbors.add("u09tu");
- expectedNeighbors.add("u09ty");
- expectedNeighbors.add("u09ts");
- expectedNeighbors.add("u09tt");
- expectedNeighbors.add("u09tw");
- neighbors = new ArrayList<>();
- GeoHashUtils.addNeighbors(geohash, neighbors );
- assertEquals(expectedNeighbors, neighbors);
-
- // Border even and odd geohash
- geohash = "ezzzz";
- expectedNeighbors = new ArrayList<>();
- expectedNeighbors.add("gbpbn");
- expectedNeighbors.add("gbpbp");
- expectedNeighbors.add("u0000");
- expectedNeighbors.add("ezzzy");
- expectedNeighbors.add("spbpb");
- expectedNeighbors.add("ezzzw");
- expectedNeighbors.add("ezzzx");
- expectedNeighbors.add("spbp8");
- neighbors = new ArrayList<>();
- GeoHashUtils.addNeighbors(geohash, neighbors );
- assertEquals(expectedNeighbors, neighbors);
- }
-
- public void testClosestPointOnBBox() {
- double[] result = new double[2];
- GeoDistanceUtils.closestPointOnBBox(20, 30, 40, 50, 70, 70, result);
- assertEquals(40.0, result[0], 0.0);
- assertEquals(50.0, result[1], 0.0);
-
- GeoDistanceUtils.closestPointOnBBox(-20, -20, 0, 0, 70, 70, result);
- assertEquals(0.0, result[0], 0.0);
- assertEquals(0.0, result[1], 0.0);
- }
-
- private static class Cell {
- static int nextCellID;
-
- final Cell parent;
- final int cellID;
- final double minLon, maxLon;
- final double minLat, maxLat;
- final int splitCount;
-
- public Cell(Cell parent,
- double minLon, double minLat,
- double maxLon, double maxLat,
- int splitCount) {
- assert maxLon >= minLon;
- assert maxLat >= minLat;
- this.parent = parent;
- this.minLon = minLon;
- this.minLat = minLat;
- this.maxLon = maxLon;
- this.maxLat = maxLat;
- this.cellID = nextCellID++;
- this.splitCount = splitCount;
- }
-
- /** Returns true if the quantized point lies within this cell, inclusive on all bounds. */
- public boolean contains(double lon, double lat) {
- return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat;
- }
-
- @Override
- public String toString() {
- return "cell=" + cellID + (parent == null ? "" : " parentCellID=" + parent.cellID) + " lon: " + minLon + " TO " + maxLon + ", lat: " + minLat + " TO " + maxLat + ", splits: " + splitCount;
- }
- }
-
- public long scaleLon(final double val) {
- return (long) ((val-GeoUtils.MIN_LON_INCL) * LON_SCALE);
- }
-
- public long scaleLat(final double val) {
- return (long) ((val-GeoUtils.MIN_LAT_INCL) * LAT_SCALE);
- }
-
- public double unscaleLon(final long val) {
- return (val / LON_SCALE) + GeoUtils.MIN_LON_INCL;
- }
-
- public double unscaleLat(final long val) {
- return (val / LAT_SCALE) + GeoUtils.MIN_LAT_INCL;
- }
-
- public double randomLat(boolean small) {
- double result;
- if (small) {
- result = GeoUtils.normalizeLat(originLat + latRange * (random().nextDouble() - 0.5));
- } else {
- result = -90 + 180.0 * random().nextDouble();
- }
- return result;
- }
-
- public double randomLon(boolean small) {
- double result;
- if (small) {
- result = GeoUtils.normalizeLon(originLon + lonRange * (random().nextDouble() - 0.5));
- } else {
- result = -180 + 360.0 * random().nextDouble();
- }
- return result;
- }
-
- private void findMatches(Set<Integer> hits, PrintWriter log, Cell root,
- double centerLon, double centerLat, double radiusMeters,
- double[] docLons, double[] docLats) {
-
- if (VERBOSE) {
- log.println(" root cell: " + root);
- }
-
- List<Cell> queue = new ArrayList<>();
- queue.add(root);
-
- int recurseDepth = RandomInts.randomIntBetween(random(), 5, 15);
-
- 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<docLons.length;docID++) {
- if (cell.contains(docLons[docID], docLats[docID])) {
- double distanceMeters = GeoDistanceUtils.haversin(centerLat, centerLon, docLats[docID], docLons[docID]);
- if (distanceMeters <= radiusMeters) {
- if (VERBOSE) {
- log.println(" check doc=" + docID + ": match!");
- }
- hits.add(docID);
- } else {
- if (VERBOSE) {
- log.println(" check doc=" + docID + ": no match");
- }
- }
- }
- }
- } else {
-
- if (GeoRelationUtils.rectWithinCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat, centerLon, centerLat, radiusMeters)) {
- // Query circle fully contains this cell, just addAll:
- if (VERBOSE) {
- log.println(" circle fully contains cell: now addAll");
- }
- for(int docID=0;docID<docLons.length;docID++) {
- if (cell.contains(docLons[docID], docLats[docID])) {
- if (VERBOSE) {
- log.println(" addAll doc=" + docID);
- }
- hits.add(docID);
- }
- }
- continue;
- } else if (GeoRelationUtils.rectWithin(root.minLon, root.minLat, root.maxLon, root.maxLat,
- cell.minLon, cell.minLat, cell.maxLon, cell.maxLat)) {
- // Fall through below to "recurse"
- if (VERBOSE) {
- log.println(" cell fully contains circle: keep splitting");
- }
- } else if (GeoRelationUtils.rectCrossesCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat,
- centerLon, centerLat, radiusMeters)) {
- // Fall through below to "recurse"
- if (VERBOSE) {
- log.println(" cell overlaps circle: keep splitting");
- }
- } else {
- if (VERBOSE) {
- log.println(" no overlap: drop this cell");
- for(int docID=0;docID<docLons.length;docID++) {
- if (cell.contains(docLons[docID], docLats[docID])) {
- if (VERBOSE) {
- log.println(" skip doc=" + docID);
- }
- }
- }
- }
- continue;
- }
-
- // Randomly split:
- if (random().nextBoolean()) {
-
- // Split on lon:
- double splitValue = cell.minLon + (cell.maxLon - cell.minLon) * random().nextDouble();
- if (VERBOSE) {
- log.println(" now split on lon=" + splitValue);
- }
- Cell cell1 = new Cell(cell,
- cell.minLon, cell.minLat,
- splitValue, cell.maxLat,
- cell.splitCount+1);
- Cell cell2 = new Cell(cell,
- splitValue, cell.minLat,
- cell.maxLon, cell.maxLat,
- cell.splitCount+1);
- if (VERBOSE) {
- log.println(" split cell1: " + cell1);
- log.println(" split cell2: " + cell2);
- }
- queue.add(cell1);
- queue.add(cell2);
- } else {
-
- // Split on lat:
- double splitValue = cell.minLat + (cell.maxLat - cell.minLat) * random().nextDouble();
- if (VERBOSE) {
- log.println(" now split on lat=" + splitValue);
- }
- Cell cell1 = new Cell(cell,
- cell.minLon, cell.minLat,
- cell.maxLon, splitValue,
- cell.splitCount+1);
- Cell cell2 = new Cell(cell,
- cell.minLon, splitValue,
- cell.maxLon, cell.maxLat,
- cell.splitCount+1);
- if (VERBOSE) {
- log.println(" split cells:\n " + cell1 + "\n " + cell2);
- }
- queue.add(cell1);
- queue.add(cell2);
- }
- }
- }
- }
-
- /** Tests consistency of GeoUtils.rectWithinCircle, .rectCrossesCircle, .rectWithin and SloppyMath.haversine distance check */
- public void testGeoRelations() throws Exception {
-
- int numDocs = atLeast(1000);
-
- boolean useSmallRanges = random().nextBoolean();
-
- if (VERBOSE) {
- System.out.println("TEST: " + numDocs + " docs useSmallRanges=" + useSmallRanges);
- }
-
- double[] docLons = new double[numDocs];
- double[] docLats = new double[numDocs];
- for(int docID=0;docID<numDocs;docID++) {
- docLons[docID] = randomLon(useSmallRanges);
- docLats[docID] = randomLat(useSmallRanges);
- if (VERBOSE) {
- System.out.println(" doc=" + docID + ": lon=" + docLons[docID] + " lat=" + docLats[docID]);
- }
- }
-
- int iters = atLeast(10);
-
- iters = atLeast(50);
-
- for(int iter=0;iter<iters;iter++) {
-
- Cell.nextCellID = 0;
-
- double centerLon = randomLon(useSmallRanges);
- double centerLat = randomLat(useSmallRanges);
-
- // So the circle covers at most 50% of the earth's surface:
-
- double radiusMeters;
-
- // TODO: large exotic rectangles created by BKD may be inaccurate up to 2 times DISTANCE_PCT_ERR.
- // restricting size until LUCENE-6994 can be addressed
- if (true || useSmallRanges) {
- // Approx 3 degrees lon at the equator:
- radiusMeters = random().nextDouble() * 333000;
- } else {
- radiusMeters = random().nextDouble() * GeoProjectionUtils.SEMIMAJOR_AXIS * Math.PI / 2.0;
- }
-
- StringWriter sw = new StringWriter();
- PrintWriter log = new PrintWriter(sw, true);
-
- if (VERBOSE) {
- log.println("\nTEST: iter=" + iter + " radiusMeters=" + radiusMeters + " centerLon=" + centerLon + " centerLat=" + centerLat);
- }
-
- GeoRect bbox = GeoUtils.circleToBBox(centerLon, centerLat, radiusMeters);
-
- Set<Integer> hits = new HashSet<>();
-
- if (bbox.maxLon < bbox.minLon) {
- // Crosses dateline
- log.println(" circle crosses dateline; first left query");
- double unwrappedLon = centerLon;
- if (unwrappedLon > bbox.maxLon) {
- // unwrap left
- unwrappedLon += -360.0D;
- }
- findMatches(hits, log,
- new Cell(null,
- -180, bbox.minLat,
- bbox.maxLon, bbox.maxLat,
- 0),
- unwrappedLon, centerLat, radiusMeters, docLons, docLats);
- log.println(" circle crosses dateline; now right query");
- if (unwrappedLon < bbox.maxLon) {
- // unwrap right
- unwrappedLon += 360.0D;
- }
- findMatches(hits, log,
- new Cell(null,
- bbox.minLon, bbox.minLat,
- 180, bbox.maxLat,
- 0),
- unwrappedLon, centerLat, radiusMeters, docLons, docLats);
- } else {
- // Start with the root cell that fully contains the shape:
- findMatches(hits, log,
- new Cell(null,
- bbox.minLon, bbox.minLat,
- bbox.maxLon, bbox.maxLat,
- 0),
- centerLon, centerLat, radiusMeters,
- docLons, docLats);
- }
-
- if (VERBOSE) {
- log.println(" " + hits.size() + " hits");
- }
-
- int failCount = 0;
-
- // Done matching, now verify:
- for(int docID=0;docID<numDocs;docID++) {
- double distanceMeters = GeoDistanceUtils.haversin(centerLat, centerLon, docLats[docID], docLons[docID]);
- final Boolean expected;
- final double percentError = Math.abs(distanceMeters - radiusMeters) / distanceMeters;
- if (percentError <= DISTANCE_PCT_ERR) {
- expected = null;
- } else {
- expected = distanceMeters <= radiusMeters;
- }
-
- boolean actual = hits.contains(docID);
- if (expected != null && actual != expected) {
- if (actual) {
- log.println("doc=" + docID + " matched but should not with distance error " + percentError + " on iteration " + iter);
- } else {
- log.println("doc=" + docID + " did not match but should with distance error " + percentError + " on iteration " + iter);
- }
- log.println(" lon=" + docLons[docID] + " lat=" + docLats[docID] + " distanceMeters=" + distanceMeters + " vs radiusMeters=" + radiusMeters);
- failCount++;
- }
- }
-
- if (failCount != 0) {
- System.out.print(sw.toString());
- fail(failCount + " incorrect hits (see above)");
- }
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/187fe876/lucene/spatial/src/java/org/apache/lucene/spatial/document/GeoPointField.java
----------------------------------------------------------------------
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/document/GeoPointField.java b/lucene/spatial/src/java/org/apache/lucene/spatial/document/GeoPointField.java
new file mode 100644
index 0000000..cf8d223
--- /dev/null
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/document/GeoPointField.java
@@ -0,0 +1,136 @@
+/*
+ * 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.spatial.document;
+
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.index.DocValuesType;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.spatial.util.GeoUtils;
+
+/**
+ * <p>
+ * Field that indexes <code>latitude</code> <code>longitude</code> decimal-degree values
+ * for efficient encoding, sorting, and querying. This Geo capability is intended
+ * to provide a basic and efficient out of the box field type for indexing and
+ * querying 2 dimensional points in WGS-84 decimal degrees. An example usage is as follows:
+ *
+ * <pre class="prettyprint">
+ * document.add(new GeoPointField(name, -96.33, 32.66, Field.Store.NO));
+ * </pre>
+ *
+ * <p>To perform simple geospatial queries against a <code>GeoPointField</code>,
+ * see {@link org.apache.lucene.spatial.search.GeoPointInBBoxQuery}, {@link org.apache.lucene.spatial.search.GeoPointInPolygonQuery},
+ * or {@link org.apache.lucene.spatial.search.GeoPointDistanceQuery}
+ *
+ * NOTE: This indexes only high precision encoded terms which may result in visiting a high number
+ * of terms for large queries. See LUCENE-6481 for a future improvement.
+ *
+ * @lucene.experimental
+ */
+public final class GeoPointField extends Field {
+ /** encoding step value for GeoPoint prefix terms */
+ public static final int PRECISION_STEP = 9;
+
+ /**
+ * Type for an GeoPointField that is not stored:
+ * normalization factors, frequencies, and positions are omitted.
+ */
+ public static final FieldType TYPE_NOT_STORED = new FieldType();
+ static {
+ TYPE_NOT_STORED.setTokenized(false);
+ TYPE_NOT_STORED.setOmitNorms(true);
+ TYPE_NOT_STORED.setIndexOptions(IndexOptions.DOCS);
+ TYPE_NOT_STORED.setDocValuesType(DocValuesType.SORTED_NUMERIC);
+ TYPE_NOT_STORED.setNumericType(FieldType.NumericType.LONG);
+ TYPE_NOT_STORED.setNumericPrecisionStep(PRECISION_STEP);
+ TYPE_NOT_STORED.freeze();
+ }
+
+ /**
+ * Type for a stored GeoPointField:
+ * normalization factors, frequencies, and positions are omitted.
+ */
+ public static final FieldType TYPE_STORED = new FieldType();
+ static {
+ TYPE_STORED.setTokenized(false);
+ TYPE_STORED.setOmitNorms(true);
+ TYPE_STORED.setIndexOptions(IndexOptions.DOCS);
+ TYPE_STORED.setDocValuesType(DocValuesType.SORTED_NUMERIC);
+ TYPE_STORED.setNumericType(FieldType.NumericType.LONG);
+ TYPE_STORED.setNumericPrecisionStep(PRECISION_STEP);
+ TYPE_STORED.setStored(true);
+ TYPE_STORED.freeze();
+ }
+
+ /** Creates a stored or un-stored GeoPointField with the provided value
+ * and default <code>precisionStep</code> set to 64 to avoid wasteful
+ * indexing of lower precision terms.
+ * @param name field name
+ * @param lon longitude double value [-180.0 : 180.0]
+ * @param lat latitude double value [-90.0 : 90.0]
+ * @param stored Store.YES if the content should also be stored
+ * @throws IllegalArgumentException if the field name is null.
+ */
+ public GeoPointField(String name, double lon, double lat, Store stored) {
+ super(name, stored == Store.YES ? TYPE_STORED : TYPE_NOT_STORED);
+ fieldsData = GeoUtils.mortonHash(lon, lat);
+ }
+
+ /** Expert: allows you to customize the {@link
+ * FieldType}.
+ * @param name field name
+ * @param lon longitude double value [-180.0 : 180.0]
+ * @param lat latitude double value [-90.0 : 90.0]
+ * @param type customized field type: must have {@link FieldType#numericType()}
+ * of {@link FieldType.NumericType#LONG}.
+ * @throws IllegalArgumentException if the field name or type is null, or
+ * if the field type does not have a LONG numericType()
+ */
+ public GeoPointField(String name, double lon, double lat, FieldType type) {
+ super(name, type);
+ if (type.numericType() != FieldType.NumericType.LONG) {
+ throw new IllegalArgumentException("type.numericType() must be LONG but got " + type.numericType());
+ }
+ if (type.docValuesType() != DocValuesType.SORTED_NUMERIC) {
+ throw new IllegalArgumentException("type.docValuesType() must be SORTED_NUMERIC but got " + type.docValuesType());
+ }
+ fieldsData = GeoUtils.mortonHash(lon, lat);
+ }
+
+ /** access longitude value */
+ public double getLon() {
+ return GeoUtils.mortonUnhashLon((long) fieldsData);
+ }
+
+ /** access latitude value */
+ public double getLat() {
+ return GeoUtils.mortonUnhashLat((long) fieldsData);
+ }
+
+ @Override
+ public String toString() {
+ if (fieldsData == null) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.append(GeoUtils.mortonUnhashLon((long) fieldsData));
+ sb.append(',');
+ sb.append(GeoUtils.mortonUnhashLat((long) fieldsData));
+ return sb.toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/187fe876/lucene/spatial/src/java/org/apache/lucene/spatial/document/package-info.java
----------------------------------------------------------------------
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/document/package-info.java b/lucene/spatial/src/java/org/apache/lucene/spatial/document/package-info.java
new file mode 100644
index 0000000..2550fa1
--- /dev/null
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/document/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Geospatial Field Implementations for Core Lucene
+ */
+package org.apache.lucene.spatial.document;
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/187fe876/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceQuery.java
----------------------------------------------------------------------
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceQuery.java b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceQuery.java
new file mode 100644
index 0000000..26b0a24
--- /dev/null
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceQuery.java
@@ -0,0 +1,186 @@
+/*
+ * 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.spatial.search;
+
+import java.io.IOException;
+
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.spatial.util.GeoDistanceUtils;
+import org.apache.lucene.spatial.util.GeoRect;
+import org.apache.lucene.spatial.util.GeoUtils;
+
+/** Implements a simple point distance query on a GeoPoint field. This is based on
+ * {@link GeoPointInBBoxQuery} and is implemented using a two phase approach. First,
+ * like {@code GeoPointInBBoxQueryImpl} candidate terms are queried using the numeric ranges based on
+ * the morton codes of the min and max lat/lon pairs that intersect the boundary of the point-radius
+ * circle. Terms
+ * passing this initial filter are then passed to a secondary {@code postFilter} method that verifies whether the
+ * decoded lat/lon point fall within the specified query distance (see {@link org.apache.lucene.util.SloppyMath#haversin}.
+ * All morton value comparisons are subject to the same precision tolerance defined in
+ * {@value org.apache.lucene.spatial.util.GeoUtils#TOLERANCE} and distance comparisons are subject to the accuracy of the
+ * haversine formula (from R.W. Sinnott, "Virtues of the Haversine", Sky and Telescope, vol. 68, no. 2, 1984, p. 159)
+ *
+ * <p>Note: This query currently uses haversine which is a sloppy distance calculation (see above reference). For large
+ * queries one can expect upwards of 400m error. Vincenty shrinks this to ~40m error but pays a penalty for computing
+ * using the spheroid
+ *
+ * @lucene.experimental */
+public class GeoPointDistanceQuery extends GeoPointInBBoxQuery {
+ /** longitude value (in degrees) for query location */
+ protected final double centerLon;
+ /** latitude value (in degrees) for query location */
+ protected final double centerLat;
+ /** distance (in meters) from lon, lat center location */
+ protected final double radiusMeters;
+
+ /**
+ * Constructs a Query for all {@link org.apache.lucene.spatial.document.GeoPointField} types within a
+ * distance (in meters) from a given point
+ **/
+ public GeoPointDistanceQuery(final String field, final double centerLon, final double centerLat, final double radiusMeters) {
+ this(field, GeoUtils.circleToBBox(centerLon, centerLat, radiusMeters), centerLon, centerLat, radiusMeters);
+ }
+
+ private GeoPointDistanceQuery(final String field, GeoRect bbox, final double centerLon,
+ final double centerLat, final double radiusMeters) {
+ super(field, bbox.minLon, bbox.minLat, bbox.maxLon, bbox.maxLat);
+ {
+ // check longitudinal overlap (limits radius)
+ final double maxRadius = GeoDistanceUtils.maxRadialDistanceMeters(centerLon, centerLat);
+ if (radiusMeters > maxRadius) {
+ throw new IllegalArgumentException("radiusMeters " + radiusMeters + " exceeds maxRadius [" + maxRadius
+ + "] at location [" + centerLon + " " + centerLat + "]");
+ }
+ }
+
+ if (GeoUtils.isValidLon(centerLon) == false) {
+ throw new IllegalArgumentException("invalid centerLon " + centerLon);
+ }
+
+ if (GeoUtils.isValidLat(centerLat) == false) {
+ throw new IllegalArgumentException("invalid centerLat " + centerLat);
+ }
+
+ if (radiusMeters <= 0.0) {
+ throw new IllegalArgumentException("invalid radiusMeters " + radiusMeters);
+ }
+
+ this.centerLon = centerLon;
+ this.centerLat = centerLat;
+ this.radiusMeters = radiusMeters;
+ }
+
+ @Override
+ public Query rewrite(IndexReader reader) throws IOException {
+ if (getBoost() != 1f) {
+ return super.rewrite(reader);
+ }
+ // query crosses dateline; split into left and right queries
+ if (maxLon < minLon) {
+ BooleanQuery.Builder bqb = new BooleanQuery.Builder();
+
+ // unwrap the longitude iff outside the specified min/max lon range
+ double unwrappedLon = centerLon;
+ if (unwrappedLon > maxLon) {
+ // unwrap left
+ unwrappedLon += -360.0D;
+ }
+ GeoPointDistanceQueryImpl left = new GeoPointDistanceQueryImpl(field, this, unwrappedLon,
+ new GeoRect(GeoUtils.MIN_LON_INCL, maxLon, minLat, maxLat));
+ bqb.add(new BooleanClause(left, BooleanClause.Occur.SHOULD));
+
+ if (unwrappedLon < maxLon) {
+ // unwrap right
+ unwrappedLon += 360.0D;
+ }
+ GeoPointDistanceQueryImpl right = new GeoPointDistanceQueryImpl(field, this, unwrappedLon,
+ new GeoRect(minLon, GeoUtils.MAX_LON_INCL, minLat, maxLat));
+ bqb.add(new BooleanClause(right, BooleanClause.Occur.SHOULD));
+
+ return bqb.build();
+ }
+ return new GeoPointDistanceQueryImpl(field, this, centerLon, new GeoRect(this.minLon, this.maxLon, this.minLat, this.maxLat));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof GeoPointDistanceQuery)) return false;
+ if (!super.equals(o)) return false;
+
+ GeoPointDistanceQuery that = (GeoPointDistanceQuery) o;
+
+ if (Double.compare(that.centerLat, centerLat) != 0) return false;
+ if (Double.compare(that.centerLon, centerLon) != 0) return false;
+ if (Double.compare(that.radiusMeters, radiusMeters) != 0) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ long temp;
+ temp = Double.doubleToLongBits(centerLon);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(centerLat);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(radiusMeters);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ @Override
+ public String toString(String field) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName());
+ sb.append(':');
+ if (!this.field.equals(field)) {
+ sb.append(" field=");
+ sb.append(this.field);
+ sb.append(':');
+ }
+ return sb.append( " Center: [")
+ .append(centerLon)
+ .append(',')
+ .append(centerLat)
+ .append(']')
+ .append(" Distance: ")
+ .append(radiusMeters)
+ .append(" meters")
+ .append("]")
+ .toString();
+ }
+
+ /** getter method for center longitude value */
+ public double getCenterLon() {
+ return this.centerLon;
+ }
+
+ /** getter method for center latitude value */
+ public double getCenterLat() {
+ return this.centerLat;
+ }
+
+ /** getter method for distance value (in meters) */
+ public double getRadiusMeters() {
+ return this.radiusMeters;
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/187fe876/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceQueryImpl.java
----------------------------------------------------------------------
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceQueryImpl.java b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceQueryImpl.java
new file mode 100644
index 0000000..0777799
--- /dev/null
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceQueryImpl.java
@@ -0,0 +1,131 @@
+/*
+ * 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.spatial.search;
+
+import java.io.IOException;
+
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.search.MultiTermQuery;
+import org.apache.lucene.util.AttributeSource;
+import org.apache.lucene.spatial.document.GeoPointField;
+import org.apache.lucene.spatial.util.GeoRect;
+import org.apache.lucene.spatial.util.GeoRelationUtils;
+import org.apache.lucene.util.SloppyMath;
+
+/** Package private implementation for the public facing GeoPointDistanceQuery delegate class.
+ *
+ * @lucene.experimental
+ */
+final class GeoPointDistanceQueryImpl extends GeoPointInBBoxQueryImpl {
+ private final GeoPointDistanceQuery query;
+ private final double centerLon;
+
+ GeoPointDistanceQueryImpl(final String field, final GeoPointDistanceQuery q, final double centerLonUnwrapped,
+ final GeoRect bbox) {
+ super(field, bbox.minLon, bbox.minLat, bbox.maxLon, bbox.maxLat);
+ query = q;
+ centerLon = centerLonUnwrapped;
+ }
+
+ @Override @SuppressWarnings("unchecked")
+ protected TermsEnum getTermsEnum(final Terms terms, AttributeSource atts) throws IOException {
+ return new GeoPointRadiusTermsEnum(terms.iterator(), this.minLon, this.minLat, this.maxLon, this.maxLat);
+ }
+
+ @Override
+ public void setRewriteMethod(MultiTermQuery.RewriteMethod method) {
+ throw new UnsupportedOperationException("cannot change rewrite method");
+ }
+
+ private final class GeoPointRadiusTermsEnum extends GeoPointTermsEnum {
+ GeoPointRadiusTermsEnum(final TermsEnum tenum, final double minLon, final double minLat,
+ final double maxLon, final double maxLat) {
+ super(tenum, minLon, minLat, maxLon, maxLat);
+ }
+
+ /**
+ * Computes the maximum shift for the given pointDistanceQuery. This prevents unnecessary depth traversal
+ * given the size of the distance query.
+ */
+ @Override
+ protected short computeMaxShift() {
+ final short shiftFactor;
+
+ if (query.radiusMeters > 1000000) {
+ shiftFactor = 5;
+ } else {
+ shiftFactor = 4;
+ }
+
+ return (short)(GeoPointField.PRECISION_STEP * shiftFactor);
+ }
+
+ @Override
+ protected boolean cellCrosses(final double minLon, final double minLat, final double maxLon, final double maxLat) {
+ return GeoRelationUtils.rectCrossesCircle(minLon, minLat, maxLon, maxLat,
+ centerLon, query.centerLat, query.radiusMeters, true);
+ }
+
+ @Override
+ protected boolean cellWithin(final double minLon, final double minLat, final double maxLon, final double maxLat) {
+ return GeoRelationUtils.rectWithinCircle(minLon, minLat, maxLon, maxLat,
+ centerLon, query.centerLat, query.radiusMeters, true);
+ }
+
+ @Override
+ protected boolean cellIntersectsShape(final double minLon, final double minLat, final double maxLon, final double maxLat) {
+ return (cellContains(minLon, minLat, maxLon, maxLat)
+ || cellWithin(minLon, minLat, maxLon, maxLat) || cellCrosses(minLon, minLat, maxLon, maxLat));
+ }
+
+ /**
+ * The two-phase query approach. The parent {@link GeoPointTermsEnum} class matches
+ * encoded terms that fall within the minimum bounding box of the point-radius circle. Those documents that pass
+ * the initial bounding box filter are then post filter compared to the provided distance using the
+ * {@link org.apache.lucene.util.SloppyMath#haversin} method.
+ */
+ @Override
+ protected boolean postFilter(final double lon, final double lat) {
+ return (SloppyMath.haversin(query.centerLat, centerLon, lat, lon) * 1000.0 <= query.radiusMeters);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof GeoPointDistanceQueryImpl)) return false;
+ if (!super.equals(o)) return false;
+
+ GeoPointDistanceQueryImpl that = (GeoPointDistanceQueryImpl) o;
+
+ if (!query.equals(that.query)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + query.hashCode();
+ return result;
+ }
+
+ public double getRadiusMeters() {
+ return query.getRadiusMeters();
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/187fe876/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceRangeQuery.java
----------------------------------------------------------------------
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceRangeQuery.java b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceRangeQuery.java
new file mode 100644
index 0000000..2173054
--- /dev/null
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointDistanceRangeQuery.java
@@ -0,0 +1,110 @@
+/*
+ * 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.spatial.search;
+
+import java.io.IOException;
+
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Query;
+
+/** Implements a point distance range query on a GeoPoint field. This is based on
+ * {@code org.apache.lucene.spatial.search.GeoPointDistanceQuery} and is implemented using a
+ * {@code org.apache.lucene.search.BooleanClause.MUST_NOT} clause to exclude any points that fall within
+ * minRadiusMeters from the provided point.
+ *
+ * @lucene.experimental
+ */
+public final class GeoPointDistanceRangeQuery extends GeoPointDistanceQuery {
+ protected final double minRadiusMeters;
+
+ /**
+ * Constructs a query for all {@link org.apache.lucene.spatial.document.GeoPointField} types within a minimum / maximum
+ * distance (in meters) range from a given point
+ */
+ public GeoPointDistanceRangeQuery(final String field, final double centerLon, final double centerLat,
+ final double minRadiusMeters, final double maxRadius) {
+ super(field, centerLon, centerLat, maxRadius);
+ this.minRadiusMeters = minRadiusMeters;
+ }
+
+ @Override
+ public Query rewrite(IndexReader reader) throws IOException {
+ if (getBoost() != 1f) {
+ return super.rewrite(reader);
+ }
+ Query q = super.rewrite(reader);
+ if (minRadiusMeters == 0.0) {
+ return q;
+ }
+
+ // add an exclusion query
+ BooleanQuery.Builder bqb = new BooleanQuery.Builder();
+
+ // create a new exclusion query
+ GeoPointDistanceQuery exclude = new GeoPointDistanceQuery(field, centerLon, centerLat, minRadiusMeters);
+ bqb.add(new BooleanClause(q, BooleanClause.Occur.MUST));
+ bqb.add(new BooleanClause(exclude, BooleanClause.Occur.MUST_NOT));
+
+ return bqb.build();
+ }
+
+ @Override
+ public String toString(String field) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName());
+ sb.append(':');
+ if (!this.field.equals(field)) {
+ sb.append(" field=");
+ sb.append(this.field);
+ sb.append(':');
+ }
+ return sb.append( " Center: [")
+ .append(centerLon)
+ .append(',')
+ .append(centerLat)
+ .append(']')
+ .append(" From Distance: ")
+ .append(minRadiusMeters)
+ .append(" m")
+ .append(" To Distance: ")
+ .append(radiusMeters)
+ .append(" m")
+ .append(" Lower Left: [")
+ .append(minLon)
+ .append(',')
+ .append(minLat)
+ .append(']')
+ .append(" Upper Right: [")
+ .append(maxLon)
+ .append(',')
+ .append(maxLat)
+ .append("]")
+ .toString();
+ }
+
+ /** getter method for minimum distance */
+ public double getMinRadiusMeters() {
+ return this.minRadiusMeters;
+ }
+
+ /** getter method for maximum distance */
+ public double getMaxRadiusMeters() {
+ return this.radiusMeters;
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/187fe876/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointInBBoxQuery.java
----------------------------------------------------------------------
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointInBBoxQuery.java b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointInBBoxQuery.java
new file mode 100644
index 0000000..a216aa0
--- /dev/null
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointInBBoxQuery.java
@@ -0,0 +1,173 @@
+/*
+ * 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.spatial.search;
+
+import java.io.IOException;
+
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.util.ToStringUtils;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.FieldValueQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.spatial.util.GeoUtils;
+
+/** Implements a simple bounding box query on a GeoPoint field. This is inspired by
+ * {@link org.apache.lucene.search.NumericRangeQuery} and is implemented using a
+ * two phase approach. First, candidate terms are queried using a numeric
+ * range based on the morton codes of the min and max lat/lon pairs. Terms
+ * passing this initial filter are passed to a final check that verifies whether
+ * the decoded lat/lon falls within (or on the boundary) of the query bounding box.
+ * The value comparisons are subject to a precision tolerance defined in
+ * {@value org.apache.lucene.spatial.util.GeoUtils#TOLERANCE}
+ *
+ * NOTES:
+ * 1. All latitude/longitude values must be in decimal degrees.
+ * 2. Complex computational geometry (e.g., dateline wrapping) is not supported
+ * 3. For more advanced GeoSpatial indexing and query operations see spatial module
+ * 4. This is well suited for small rectangles, large bounding boxes may result
+ * in many terms, depending whether the bounding box falls on the boundary of
+ * many cells (degenerate case)
+ *
+ * @lucene.experimental
+ */
+public class GeoPointInBBoxQuery extends Query {
+ protected final String field;
+ protected final double minLon;
+ protected final double minLat;
+ protected final double maxLon;
+ protected final double maxLat;
+
+ /**
+ * Constructs a query for all {@link org.apache.lucene.spatial.document.GeoPointField} types that fall within a
+ * defined bounding box
+ */
+ public GeoPointInBBoxQuery(final String field, final double minLon, final double minLat, final double maxLon, final double maxLat) {
+ this.field = field;
+ this.minLon = minLon;
+ this.minLat = minLat;
+ this.maxLon = maxLon;
+ this.maxLat = maxLat;
+ }
+
+ @Override
+ public Query rewrite(IndexReader reader) throws IOException {
+ if (getBoost() != 1f) {
+ return super.rewrite(reader);
+ }
+
+ // short-circuit to match all if specifying the whole map
+ if (minLon == GeoUtils.MIN_LON_INCL && maxLon == GeoUtils.MAX_LON_INCL
+ && minLat == GeoUtils.MIN_LAT_INCL && maxLat == GeoUtils.MAX_LAT_INCL) {
+ // FieldValueQuery is valid since DocValues are *required* for GeoPointField
+ return new FieldValueQuery(field);
+ }
+
+ if (maxLon < minLon) {
+ BooleanQuery.Builder bq = new BooleanQuery.Builder();
+
+ GeoPointInBBoxQueryImpl left = new GeoPointInBBoxQueryImpl(field, -180.0D, minLat, maxLon, maxLat);
+ bq.add(new BooleanClause(left, BooleanClause.Occur.SHOULD));
+ GeoPointInBBoxQueryImpl right = new GeoPointInBBoxQueryImpl(field, minLon, minLat, 180.0D, maxLat);
+ bq.add(new BooleanClause(right, BooleanClause.Occur.SHOULD));
+ return bq.build();
+ }
+ return new GeoPointInBBoxQueryImpl(field, minLon, minLat, maxLon, maxLat);
+ }
+
+ @Override
+ public String toString(String field) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName());
+ sb.append(':');
+ if (!this.field.equals(field)) {
+ sb.append(" field=");
+ sb.append(this.field);
+ sb.append(':');
+ }
+ return sb.append(" Lower Left: [")
+ .append(minLon)
+ .append(',')
+ .append(minLat)
+ .append(']')
+ .append(" Upper Right: [")
+ .append(maxLon)
+ .append(',')
+ .append(maxLat)
+ .append("]")
+ .append(ToStringUtils.boost(getBoost()))
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof GeoPointInBBoxQuery)) return false;
+ if (!super.equals(o)) return false;
+
+ GeoPointInBBoxQuery that = (GeoPointInBBoxQuery) o;
+
+ if (Double.compare(that.maxLat, maxLat) != 0) return false;
+ if (Double.compare(that.maxLon, maxLon) != 0) return false;
+ if (Double.compare(that.minLat, minLat) != 0) return false;
+ if (Double.compare(that.minLon, minLon) != 0) return false;
+ if (!field.equals(that.field)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ long temp;
+ result = 31 * result + field.hashCode();
+ temp = Double.doubleToLongBits(minLon);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(minLat);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(maxLon);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(maxLat);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ /** getter method for retrieving the field name */
+ public final String getField() {
+ return this.field;
+ }
+
+ /** getter method for retrieving the minimum longitude (in degrees) */
+ public final double getMinLon() {
+ return this.minLon;
+ }
+
+ /** getter method for retrieving the minimum latitude (in degrees) */
+ public final double getMinLat() {
+ return this.minLat;
+ }
+
+ /** getter method for retrieving the maximum longitude (in degrees) */
+ public final double getMaxLon() {
+ return this.maxLon;
+ }
+
+ /** getter method for retrieving the maximum latitude (in degrees) */
+ public final double getMaxLat() {
+ return this.maxLat;
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/187fe876/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointInBBoxQueryImpl.java
----------------------------------------------------------------------
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointInBBoxQueryImpl.java b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointInBBoxQueryImpl.java
new file mode 100644
index 0000000..bd44b1e
--- /dev/null
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/search/GeoPointInBBoxQueryImpl.java
@@ -0,0 +1,161 @@
+/*
+ * 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.spatial.search;
+
+import java.io.IOException;
+
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.search.MultiTermQuery;
+import org.apache.lucene.util.AttributeSource;
+import org.apache.lucene.util.SloppyMath;
+import org.apache.lucene.spatial.document.GeoPointField;
+import org.apache.lucene.spatial.util.GeoRelationUtils;
+
+/** Package private implementation for the public facing GeoPointInBBoxQuery delegate class.
+ *
+ * @lucene.experimental
+ */
+class GeoPointInBBoxQueryImpl extends GeoPointTermQuery {
+ /**
+ * Constructs a new GeoBBoxQuery that will match encoded GeoPoint terms that fall within or on the boundary
+ * of the bounding box defined by the input parameters
+ * @param field the field name
+ * @param minLon lower longitude (x) value of the bounding box
+ * @param minLat lower latitude (y) value of the bounding box
+ * @param maxLon upper longitude (x) value of the bounding box
+ * @param maxLat upper latitude (y) value of the bounding box
+ */
+ GeoPointInBBoxQueryImpl(final String field, final double minLon, final double minLat, final double maxLon, final double maxLat) {
+ super(field, minLon, minLat, maxLon, maxLat);
+ }
+
+ @Override @SuppressWarnings("unchecked")
+ protected TermsEnum getTermsEnum(final Terms terms, AttributeSource atts) throws IOException {
+ return new GeoPointInBBoxTermsEnum(terms.iterator(), minLon, minLat, maxLon, maxLat);
+ }
+
+ @Override
+ public void setRewriteMethod(MultiTermQuery.RewriteMethod method) {
+ throw new UnsupportedOperationException("cannot change rewrite method");
+ }
+
+ protected class GeoPointInBBoxTermsEnum extends GeoPointTermsEnum {
+ protected GeoPointInBBoxTermsEnum(final TermsEnum tenum, final double minLon, final double minLat,
+ final double maxLon, final double maxLat) {
+ super(tenum, minLon, minLat, maxLon, maxLat);
+ }
+
+ @Override
+ protected short computeMaxShift() {
+ final short shiftFactor;
+
+ // compute diagonal radius
+ double midLon = (minLon + maxLon) * 0.5;
+ double midLat = (minLat + maxLat) * 0.5;
+
+ if (SloppyMath.haversin(minLat, minLon, midLat, midLon)*1000 > 1000000) {
+ shiftFactor = 5;
+ } else {
+ shiftFactor = 4;
+ }
+
+ return (short)(GeoPointField.PRECISION_STEP * shiftFactor);
+ }
+
+ /**
+ * Determine whether the quad-cell crosses the shape
+ */
+ @Override
+ protected boolean cellCrosses(final double minLon, final double minLat, final double maxLon, final double maxLat) {
+ return GeoRelationUtils.rectCrosses(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat);
+ }
+
+ /**
+ * Determine whether quad-cell is within the shape
+ */
+ @Override
+ protected boolean cellWithin(final double minLon, final double minLat, final double maxLon, final double maxLat) {
+ return GeoRelationUtils.rectWithin(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat);
+ }
+
+ @Override
+ protected boolean cellIntersectsShape(final double minLon, final double minLat, final double maxLon, final double maxLat) {
+ return cellIntersectsMBR(minLon, minLat, maxLon, maxLat);
+ }
+
+ @Override
+ protected boolean postFilter(final double lon, final double lat) {
+ return GeoRelationUtils.pointInRectPrecise(lon, lat, minLon, minLat, maxLon, maxLat);
+ }
+ }
+
+ @Override
+ @SuppressWarnings({"unchecked","rawtypes"})
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+
+ GeoPointInBBoxQueryImpl that = (GeoPointInBBoxQueryImpl) o;
+
+ if (Double.compare(that.maxLat, maxLat) != 0) return false;
+ if (Double.compare(that.maxLon, maxLon) != 0) return false;
+ if (Double.compare(that.minLat, minLat) != 0) return false;
+ if (Double.compare(that.minLon, minLon) != 0) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ long temp;
+ temp = Double.doubleToLongBits(minLon);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(minLat);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(maxLon);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(maxLat);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ @Override
+ public String toString(String field) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName());
+ sb.append(':');
+ if (!getField().equals(field)) {
+ sb.append(" field=");
+ sb.append(getField());
+ sb.append(':');
+ }
+ return sb.append(" Lower Left: [")
+ .append(minLon)
+ .append(',')
+ .append(minLat)
+ .append(']')
+ .append(" Upper Right: [")
+ .append(maxLon)
+ .append(',')
+ .append(maxLat)
+ .append("]")
+ .toString();
+ }
+}