diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 6d4ea4171ec..9c8ab93c3bd 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -57,6 +57,10 @@ New Features * LUCENE-6526: Asserting(Query|Weight|Scorer) now ensure scores are not computed if they are not needed. (Adrien Grand) +* LUCENE-6481: Add GeoPointField, GeoPointInBBoxQuery, + GeoPointInPolygonQuery for simple "indexed lat/lon point in + bbox/shape" searching. (Nick Knize via Mike McCandless) + API Changes * LUCENE-6508: Simplify Lock api, there is now just diff --git a/lucene/core/src/java/org/apache/lucene/search/NumericRangeQuery.java b/lucene/core/src/java/org/apache/lucene/search/NumericRangeQuery.java index 45ed87b1672..93dee178c3e 100644 --- a/lucene/core/src/java/org/apache/lucene/search/NumericRangeQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/NumericRangeQuery.java @@ -167,8 +167,7 @@ import org.apache.lucene.index.Term; // for javadocs public final class NumericRangeQuery extends MultiTermQuery { private NumericRangeQuery(final String field, final int precisionStep, final NumericType dataType, - T min, T max, final boolean minInclusive, final boolean maxInclusive - ) { + T min, T max, final boolean minInclusive, final boolean maxInclusive) { super(field); if (precisionStep < 1) throw new IllegalArgumentException("precisionStep must be >=1"); diff --git a/lucene/core/src/java/org/apache/lucene/util/BitUtil.java b/lucene/core/src/java/org/apache/lucene/util/BitUtil.java index 5b0dc78a465..d412ac1c63a 100644 --- a/lucene/core/src/java/org/apache/lucene/util/BitUtil.java +++ b/lucene/core/src/java/org/apache/lucene/util/BitUtil.java @@ -22,6 +22,15 @@ package org.apache.lucene.util; // from org.apache.solr.util rev 555343 */ public final class BitUtil { + // magic numbers for bit interleaving + private static final long MAGIC[] = { + 0x5555555555555555L, 0x3333333333333333L, + 0x0F0F0F0F0F0F0F0FL, 0x00FF00FF00FF00FFL, + 0x0000FFFF0000FFFFL, 0x00000000FFFFFFFFL + }; + // shift values for bit interleaving + private static final short SHIFT[] = {1, 2, 4, 8, 16}; + private BitUtil() {} // no instance // The pop methods used to rely on bit-manipulation tricks for speed but it @@ -102,6 +111,39 @@ public final class BitUtil { return v; } + /** + * Interleaves the first 32 bits of each long value + * + * Adapted from: http://graphics.stanford.edu/~seander/bithacks.html#InterleaveBMN + */ + public static long interleave(long v1, long v2) { + v1 = (v1 | (v1 << SHIFT[4])) & MAGIC[4]; + v1 = (v1 | (v1 << SHIFT[3])) & MAGIC[3]; + v1 = (v1 | (v1 << SHIFT[2])) & MAGIC[2]; + v1 = (v1 | (v1 << SHIFT[1])) & MAGIC[1]; + v1 = (v1 | (v1 << SHIFT[0])) & MAGIC[0]; + v2 = (v2 | (v2 << SHIFT[4])) & MAGIC[4]; + v2 = (v2 | (v2 << SHIFT[3])) & MAGIC[3]; + v2 = (v2 | (v2 << SHIFT[2])) & MAGIC[2]; + v2 = (v2 | (v2 << SHIFT[1])) & MAGIC[1]; + v2 = (v2 | (v2 << SHIFT[0])) & MAGIC[0]; + + return (v2<<1) | v1; + } + + /** + * Deinterleaves long value back to two concatenated 32bit values + */ + public static long deinterleave(long b) { + b &= MAGIC[0]; + b = (b ^ (b >>> SHIFT[0])) & MAGIC[1]; + b = (b ^ (b >>> SHIFT[1])) & MAGIC[2]; + b = (b ^ (b >>> SHIFT[2])) & MAGIC[3]; + b = (b ^ (b >>> SHIFT[3])) & MAGIC[4]; + b = (b ^ (b >>> SHIFT[4])) & MAGIC[5]; + return b; + } + /** Same as {@link #zigZagEncode(long)} but on integers. */ public static int zigZagEncode(int i) { return (i >> 31) ^ (i << 1); diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/GeoPointField.java b/lucene/sandbox/src/java/org/apache/lucene/document/GeoPointField.java new file mode 100644 index 00000000000..15775e9306d --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/GeoPointField.java @@ -0,0 +1,105 @@ +package org.apache.lucene.document; + +/* + * 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 org.apache.lucene.index.IndexOptions; +import org.apache.lucene.util.GeoUtils; + +/** + *

+ * Field that indexes latitude longitude 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: + * + *

+ *  document.add(new GeoPointField(name, -96.33, 32.66, Field.Store.NO));
+ * 
+ * + *

To perform simple geospatial queries against a GeoPointField, + * see {@link org.apache.lucene.search.GeoPointInBBoxQuery} or {@link org.apache.lucene.search.GeoPointInPolygonQuery} + * + * 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 { + public static final int PRECISION_STEP = 6; + + /** + * 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.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.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 precisionStep 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()); + } + fieldsData = GeoUtils.mortonHash(lon, lat); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/package.html b/lucene/sandbox/src/java/org/apache/lucene/document/package.html new file mode 100644 index 00000000000..6f261284919 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/document/package.html @@ -0,0 +1,28 @@ + + + + + + + + + + +This package contains a single Field implementation, GeoPointField, to index a lat/lon geospatial point. + + diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/GeoBoundingBox.java b/lucene/sandbox/src/java/org/apache/lucene/search/GeoBoundingBox.java new file mode 100644 index 00000000000..3e3650d1cc0 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/search/GeoBoundingBox.java @@ -0,0 +1,47 @@ +package org.apache.lucene.search; + +/* + * 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 org.apache.lucene.util.GeoUtils; + +/** NOTE: package private; just used so {@link GeoPointInPolygonQuery} can communicate its bounding box to {@link GeoPointInBBoxQuery}. */ +class GeoBoundingBox { + public final double minLon; + public final double maxLon; + public final double minLat; + public final double maxLat; + + public GeoBoundingBox(double minLon, double maxLon, double minLat, double maxLat) { + if (GeoUtils.isValidLon(minLon) == false) { + throw new IllegalArgumentException("invalid minLon " + minLon); + } + if (GeoUtils.isValidLon(maxLon) == false) { + throw new IllegalArgumentException("invalid maxLon " + minLon); + } + if (GeoUtils.isValidLat(minLat) == false) { + throw new IllegalArgumentException("invalid minLat " + minLat); + } + if (GeoUtils.isValidLat(maxLat) == false) { + throw new IllegalArgumentException("invalid maxLat " + minLat); + } + this.minLon = minLon; + this.maxLon = maxLon; + this.minLat = minLat; + this.maxLat = maxLat; + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/GeoPointInBBoxQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/GeoPointInBBoxQuery.java new file mode 100644 index 00000000000..33c687a885f --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/search/GeoPointInBBoxQuery.java @@ -0,0 +1,148 @@ +package org.apache.lucene.search; + +/* + * 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 java.io.IOException; + +import org.apache.lucene.index.Terms; +import org.apache.lucene.index.TermsEnum; +import org.apache.lucene.util.AttributeSource; +import org.apache.lucene.util.GeoUtils; +import org.apache.lucene.util.ToStringUtils; + +/** 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.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 MultiTermQuery { + // simple bounding box optimization - no objects used to avoid dependencies + protected final double minLon; + protected final double minLat; + protected final double maxLon; + protected final double maxLat; + + /** + * 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 + */ + public GeoPointInBBoxQuery(final String field, final double minLon, final double minLat, final double maxLon, final double maxLat) { + super(field); + if (GeoUtils.isValidLon(minLon) == false) { + throw new IllegalArgumentException("invalid minLon " + minLon); + } + if (GeoUtils.isValidLon(maxLon) == false) { + throw new IllegalArgumentException("invalid maxLon " + maxLon); + } + if (GeoUtils.isValidLat(minLat) == false) { + throw new IllegalArgumentException("invalid minLat " + minLat); + } + if (GeoUtils.isValidLat(maxLat) == false) { + throw new IllegalArgumentException("invalid maxLat " + maxLat); + } + this.minLon = minLon; + this.minLat = minLat; + this.maxLon = maxLon; + this.maxLat = maxLat; + } + + @Override @SuppressWarnings("unchecked") + protected TermsEnum getTermsEnum(final Terms terms, AttributeSource atts) throws IOException { + final Long min = GeoUtils.mortonHash(minLon, minLat); + final Long max = Math.abs(GeoUtils.mortonHash(maxLon, maxLat)); + if (min != null && max != null && min.compareTo(max) > 0) { + return TermsEnum.EMPTY; + } + return new GeoPointTermsEnum(terms.iterator(), atts, 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; + + 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; + + 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("]") + .append(ToStringUtils.boost(getBoost())) + .toString(); + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/GeoPointInPolygonQuery.java b/lucene/sandbox/src/java/org/apache/lucene/search/GeoPointInPolygonQuery.java new file mode 100644 index 00000000000..327c681302c --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/search/GeoPointInPolygonQuery.java @@ -0,0 +1,250 @@ +package org.apache.lucene.search; + +/* + * 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 org.apache.lucene.index.Terms; +import org.apache.lucene.index.TermsEnum; +import org.apache.lucene.util.AttributeSource; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.GeoUtils; +import org.apache.lucene.util.NumericUtils; +import org.apache.lucene.util.ToStringUtils; + +import java.io.IOException; +import java.util.Arrays; + +/** Implements a simple point in polygon query on a GeoPoint field. This is based on + * {@link GeoPointInBBoxQuery} and is implemented using a + * three phase approach. First, like {@link GeoPointInBBoxQuery} + * 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 secondary filter that verifies whether the decoded lat/lon point falls within + * (or on the boundary) of the bounding box query. Finally, the remaining candidate + * term is passed to the final point in polygon check. All value comparisons are subject + * to the same precision tolerance defined in {@value org.apache.lucene.util.GeoUtils#TOLERANCE} + * + * NOTES: + * 1. The polygon coordinates need to be in either clockwise or counter-clockwise order. + * 2. The polygon must not be self-crossing, otherwise the query may result in unexpected behavior + * 3. All latitude/longitude values must be in decimal degrees. + * 4. Complex computational geometry (e.g., dateline wrapping, polygon with holes) is not supported + * 5. For more advanced GeoSpatial indexing and query operations see spatial module + * + * @lucene.experimental + */ +public final class GeoPointInPolygonQuery extends GeoPointInBBoxQuery { + // polygon position arrays - this avoids the use of any objects or + // or geo library dependencies + private final double[] x; + private final double[] y; + + /** + * Constructs a new GeoPolygonQuery that will match encoded {@link org.apache.lucene.document.GeoPointField} terms + * that fall within or on the boundary of the polygon defined by the input parameters. + */ + public GeoPointInPolygonQuery(final String field, final double[] polyLons, final double[] polyLats) { + this(field, computeBBox(polyLons, polyLats), polyLons, polyLats); + } + + /** + * Expert: constructs a new GeoPolygonQuery that will match encoded {@link org.apache.lucene.document.GeoPointField} terms + * that fall within or on the boundary of the polygon defined by the input parameters. This constructor requires a + * precomputed bounding box. As an alternative, {@link #GeoPointInPolygonQuery(String,double[],double[])} can + * be used to compute the bounding box during construction + * + * @param field the field name + * @param minLon lower longitude (x) value of the bounding box optimizer + * @param minLat lower latitude (y) value of the bounding box optimizer + * @param maxLon upper longitude (x) value of the bounding box optimizer + * @param maxLat upper latitude (y) value of the bounding box optimizer + * @param polyLons array containing all longitude values for the polygon + * @param polyLats array containing all latitude values for the polygon + */ + public GeoPointInPolygonQuery(final String field, final double minLon, final double minLat, final double maxLon, + final double maxLat, final double[] polyLons, final double[] polyLats) { + // TODO: should we remove this? It's dangerous .. app could accidentally provide too-small bbox? + // we should at least verify that bbox does in fact fully contain the poly? + this(field, new GeoBoundingBox(minLon, maxLon, minLat, maxLat), polyLons, polyLats); + } + + /** Common constructor, used only internally. */ + private GeoPointInPolygonQuery(final String field, GeoBoundingBox bbox, final double[] polyLons, final double[] polyLats) { + super(field, bbox.minLon, bbox.minLat, bbox.maxLon, bbox.maxLat); + if (polyLats.length != polyLons.length) { + throw new IllegalArgumentException("polyLats and polyLons must be equal length"); + } + if (polyLats.length < 4) { + throw new IllegalArgumentException("at least 4 polygon points required"); + } + if (polyLats[0] != polyLats[polyLats.length-1]) { + throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLats[0]=" + polyLats[0] + " polyLats[" + (polyLats.length-1) + "]=" + polyLats[polyLats.length-1]); + } + if (polyLons[0] != polyLons[polyLons.length-1]) { + throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLons[0]=" + polyLons[0] + " polyLons[" + (polyLons.length-1) + "]=" + polyLons[polyLons.length-1]); + } + + // convert polygon vertices to coordinates within tolerance + this.x = toleranceConversion(polyLons); + this.y = toleranceConversion(polyLats); + } + + private double[] toleranceConversion(double[] vals) { + for (int i=0; i 0) { + return TermsEnum.EMPTY; + } + return new GeoPolygonTermsEnum(terms.iterator(), atts, minLon, minLat, maxLon, maxLat); + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + GeoPointInPolygonQuery that = (GeoPointInPolygonQuery) o; + + if (!Arrays.equals(x, that.x)) return false; + if (!Arrays.equals(y, that.y)) return false; + + return true; + } + + @Override + public final int hashCode() { + int result = super.hashCode(); + result = 31 * result + (x != null ? Arrays.hashCode(x) : 0); + result = 31 * result + (y != null ? Arrays.hashCode(y) : 0); + return result; + } + + @Override + public String toString(String field) { + assert x.length == y.length; + + final StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()); + sb.append(':'); + if (!getField().equals(field)) { + sb.append(" field="); + sb.append(getField()); + sb.append(':'); + } + sb.append(" Points: "); + for (int i=0; i rangeBounds; + + private static final short DETAIL_LEVEL = 16; + + GeoPointTermsEnum(final TermsEnum tenum, AttributeSource atts, final double minLon, final double minLat, + final double maxLon, final double maxLat) { + super(tenum); + final long rectMinHash = GeoUtils.mortonHash(minLon, minLat); + final long rectMaxHash = GeoUtils.mortonHash(maxLon, maxLat); + this.minLon = GeoUtils.mortonUnhashLon(rectMinHash); + this.minLat = GeoUtils.mortonUnhashLat(rectMinHash); + this.maxLon = GeoUtils.mortonUnhashLon(rectMaxHash); + this.maxLat = GeoUtils.mortonUnhashLat(rectMaxHash); + + this.rangesAtt = atts.addAttribute(ComputedRangesAttribute.class); + this.rangeBounds = rangesAtt.ranges(); + + if (rangeBounds.isEmpty()) { + computeRange(0L, (short) (((GeoUtils.BITS) << 1) - 1)); + Collections.sort(rangeBounds); + } + } + + /** + * entry point for recursively computing ranges + */ + private final void computeRange(long term, final short shift) { + final long split = term | (0x1L<>>1); + + // if cell is within and a factor of the precision step, add the range + // if cell cellCrosses + + final boolean within = res% GeoPointField.PRECISION_STEP == 0 && cellWithin(minLon, minLat, maxLon, maxLat); + if (within || (level == DETAIL_LEVEL && cellCrosses(minLon, minLat, maxLon, maxLat))) { + rangeBounds.add(new Range(start, end, res, level, !within)); + } else if (level <= DETAIL_LEVEL && cellIntersects(minLon, minLat, maxLon, maxLat)) { + computeRange(start, (short)(res - 1)); + } + } + + protected boolean cellCrosses(final double minLon, final double minLat, final double maxLon, final double maxLat) { + return GeoUtils.rectCrosses(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat); + } + + protected boolean cellWithin(final double minLon, final double minLat, final double maxLon, final double maxLat) { + return GeoUtils.rectWithin(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat); + } + + protected boolean cellIntersects(final double minLon, final double minLat, final double maxLon, final double maxLat) { + return GeoUtils.rectIntersects(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat); + } + + private void nextRange() { + currentRange = rangeBounds.removeFirst(); + currentLowerBound = currentRange.lower; + assert currentUpperBound == null || currentUpperBound.compareTo(currentRange.lower) <= 0 : + "The current upper bound must be <= the new lower bound"; + + currentUpperBound = currentRange.upper; + } + + @Override + protected final BytesRef nextSeekTerm(BytesRef term) { + while (!rangeBounds.isEmpty()) { + if (currentRange == null) { + nextRange(); + } + + // if the new upper bound is before the term parameter, the sub-range is never a hit + if (term != null && term.compareTo(currentUpperBound) > 0) { + nextRange(); + if (!rangeBounds.isEmpty()) { + continue; + } + } + // never seek backwards, so use current term if lower bound is smaller + return (term != null && term.compareTo(currentLowerBound) > 0) ? + term : currentLowerBound; + } + + // no more sub-range enums available + assert rangeBounds.isEmpty(); + currentLowerBound = currentUpperBound = null; + return null; + } + + /** + * The two-phase query approach. {@link #nextSeekTerm} is called to obtain the next term that matches a numeric + * range of the bounding box. Those terms that pass the initial range filter are then compared against the + * decoded min/max latitude and longitude values of the bounding box only if the range is not a "boundary" range + * (e.g., a range that straddles the boundary of the bbox). + * @param term term for candidate document + * @return match status + */ + @Override + protected AcceptStatus accept(BytesRef term) { + // validate value is in range + while (currentUpperBound == null || term.compareTo(currentUpperBound) > 0) { + if (rangeBounds.isEmpty()) + return AcceptStatus.END; + // peek next sub-range, only seek if the current term is smaller than next lower bound + if (term.compareTo(rangeBounds.getFirst().lower) < 0) + return AcceptStatus.NO_AND_SEEK; + // step forward to next range without seeking, as next lower range bound is less or equal current term + nextRange(); + } + + // final-filter boundary ranges by bounding box + if (currentRange.boundary) { + final long val = NumericUtils.prefixCodedToLong(term); + final double lon = GeoUtils.mortonUnhashLon(val); + final double lat = GeoUtils.mortonUnhashLat(val); + if (!GeoUtils.bboxContains(lon, lat, minLon, minLat, maxLon, maxLat)) { + return AcceptStatus.NO; + } + } + return AcceptStatus.YES; + } + + public static interface ComputedRangesAttribute extends Attribute { + public LinkedList ranges(); + } + + @SuppressWarnings({"unchecked","rawtypes"}) + public static final class ComputedRangesAttributeImpl extends AttributeImpl implements ComputedRangesAttribute { + public final LinkedList rangeBounds = new LinkedList(); + + @Override + public LinkedList ranges() { + return rangeBounds; + } + + @Override + public void clear() { + rangeBounds.clear(); + } + + @Override + public int hashCode() { + return rangeBounds.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (!(other instanceof ComputedRangesAttributeImpl)) + return false; + return rangeBounds.equals(((ComputedRangesAttributeImpl)other).rangeBounds); + } + + @Override + public void copyTo(AttributeImpl target) { + final List targetRanges = ((ComputedRangesAttribute)target).ranges(); + targetRanges.clear(); + targetRanges.addAll(rangeBounds); + } + } + + /** + * Internal class to represent a range along the space filling curve + */ + private final class Range implements Comparable { + final BytesRef lower; + final BytesRef upper; + final short level; + final boolean boundary; + + Range(final long lower, final long upper, final short res, final short level, boolean boundary) { + this.level = level; + this.boundary = boundary; + + BytesRefBuilder brb = new BytesRefBuilder(); + NumericUtils.longToPrefixCodedBytes(lower, boundary ? 0 : res, brb); + this.lower = brb.get(); + NumericUtils.longToPrefixCodedBytes(upper, boundary ? 0 : res, (brb = new BytesRefBuilder())); + this.upper = brb.get(); + } + + @Override + public final int compareTo(Range other) { + return this.lower.compareTo(other.lower); + } + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/package.html b/lucene/sandbox/src/java/org/apache/lucene/search/package.html index 0ae2dd61765..e4a0c8e2831 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/search/package.html +++ b/lucene/sandbox/src/java/org/apache/lucene/search/package.html @@ -23,6 +23,6 @@ -This package contains a single proximity query, TermAutomatonQuery. +This package contains a flexible graph-based proximity query, TermAutomatonQuery, and geospatial queries. diff --git a/lucene/sandbox/src/java/org/apache/lucene/util/GeoUtils.java b/lucene/sandbox/src/java/org/apache/lucene/util/GeoUtils.java new file mode 100644 index 00000000000..2bee1003aed --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/util/GeoUtils.java @@ -0,0 +1,351 @@ +package org.apache.lucene.util; + +/* + * 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. + */ + +/** + * Basic reusable geo-spatial utility methods + * + * @lucene.experimental + */ +public final class GeoUtils { + // WGS84 earth-ellipsoid major (a) minor (b) radius, (f) flattening and eccentricity (e) + private static final double SEMIMAJOR_AXIS = 6_378_137; // [m] + private static final double FLATTENING = 1.0/298.257223563; + private static final double SEMIMINOR_AXIS = SEMIMAJOR_AXIS * (1.0 - FLATTENING); //6_356_752.31420; // [m] + private static final double ECCENTRICITY = StrictMath.sqrt((2.0 - FLATTENING) * FLATTENING); + private static final double PI_OVER_2 = StrictMath.PI / 2.0D; + private static final double SEMIMAJOR_AXIS2 = SEMIMAJOR_AXIS * SEMIMINOR_AXIS; + private static final double SEMIMINOR_AXIS2 = SEMIMINOR_AXIS * SEMIMINOR_AXIS; + + private static final short MIN_LON = -180; + private static final short MIN_LAT = -90; + public static final short BITS = 31; + private static final double LON_SCALE = (0x1L<>> 1)); + } + + private static long scaleLon(final double val) { + return (long) ((val-MIN_LON) * LON_SCALE); + } + + private static long scaleLat(final double val) { + return (long) ((val-MIN_LAT) * LAT_SCALE); + } + + private static double unscaleLon(final long val) { + return (val / LON_SCALE) + MIN_LON; + } + + private static double unscaleLat(final long val) { + return (val / LAT_SCALE) + MIN_LAT; + } + + public static final double compare(final double v1, final double v2) { + final double compare = v1-v2; + return Math.abs(compare) <= TOLERANCE ? 0 : compare; + } + + public static final boolean bboxContains(final double lon, final double lat, final double minLon, + final double minLat, final double maxLon, final double maxLat) { + return (compare(lon, minLon) >= 0 && compare(lon, maxLon) <= 0 + && compare(lat, minLat) >= 0 && compare(lat, maxLat) <= 0); + } + + /** + * Converts from geodesic lon lat alt to geocentric earth-centered earth-fixed + * @param lon geodesic longitude + * @param lat geodesic latitude + * @param alt geodesic altitude + * @param ecf reusable earth-centered earth-fixed result + * @return either a new ecef array or the reusable ecf parameter + */ + public static final double[] llaToECF(double lon, double lat, double alt, double[] ecf) { + lon = StrictMath.toRadians(lon); + lat = StrictMath.toRadians(lat); + + final double sl = StrictMath.sin(lat); + final double s2 = sl*sl; + final double cl = StrictMath.cos(lat); + final double ge2 = (SEMIMAJOR_AXIS2 - SEMIMINOR_AXIS2)/(SEMIMAJOR_AXIS2); + + if (ecf == null) + ecf = new double[3]; + + if (lat < -PI_OVER_2 && lat > -1.001D * PI_OVER_2) { + lat = -PI_OVER_2; + } else if (lat > PI_OVER_2 && lat < 1.001D * PI_OVER_2) { + lat = PI_OVER_2; + } + assert ((lat >= -PI_OVER_2) || (lat <= PI_OVER_2)); + + if (lon > StrictMath.PI) { + lon -= (2*StrictMath.PI); + } + + final double rn = SEMIMAJOR_AXIS / StrictMath.sqrt(1.0D - ge2 * s2); + ecf[0] = (rn+alt) * cl * StrictMath.cos(lon); + ecf[1] = (rn+alt) * cl * StrictMath.sin(lon); + ecf[2] = ((rn*(1.0-ge2))+alt)*sl; + + return ecf; + } + + /** + * Converts from geocentric earth-centered earth-fixed to geodesic lat/lon/alt + * @param x Cartesian x coordinate + * @param y Cartesian y coordinate + * @param z Cartesian z coordinate + * @param lla 0: longitude 1: latitude: 2: altitude + * @return double array as 0: longitude 1: latitude 2: altitude + */ + public static final double[] ecfToLLA(final double x, final double y, final double z, double[] lla) { + boolean atPole = false; + final double ad_c = 1.0026000D; + final double e2 = (SEMIMAJOR_AXIS2 - SEMIMINOR_AXIS2)/(SEMIMAJOR_AXIS2); + final double ep2 = (SEMIMAJOR_AXIS2 - SEMIMINOR_AXIS2)/(SEMIMINOR_AXIS2); + final double cos67P5 = 0.38268343236508977D; + + if (lla == null) + lla = new double[3]; + + if (x != 0.0) { + lla[0] = StrictMath.atan2(y,x); + } else { + if (y > 0) { + lla[0] = PI_OVER_2; + } else if (y < 0) { + lla[0] = -PI_OVER_2; + } else { + atPole = true; + lla[0] = 0.0D; + if (z > 0.0) { + lla[1] = PI_OVER_2; + } else if (z < 0.0) { + lla[1] = -PI_OVER_2; + } else { + lla[1] = PI_OVER_2; + lla[2] = -SEMIMINOR_AXIS; + return lla; + } + } + } + + final double w2 = x*x + y*y; + final double w = StrictMath.sqrt(w2); + final double t0 = z * ad_c; + final double s0 = StrictMath.sqrt(t0 * t0 + w2); + final double sinB0 = t0 / s0; + final double cosB0 = w / s0; + final double sin3B0 = sinB0 * sinB0 * sinB0; + final double t1 = z + SEMIMINOR_AXIS * ep2 * sin3B0; + final double sum = w - SEMIMAJOR_AXIS * e2 * cosB0 * cosB0 * cosB0; + final double s1 = StrictMath.sqrt(t1 * t1 + sum * sum); + final double sinP1 = t1 / s1; + final double cosP1 = sum / s1; + final double rn = SEMIMAJOR_AXIS / StrictMath.sqrt(1.0D - e2 * sinP1 * sinP1); + + if (cosP1 >= cos67P5) { + lla[2] = w / cosP1 - rn; + } else if (cosP1 <= -cos67P5) { + lla[2] = w / -cosP1 - rn; + } else { + lla[2] = z / sinP1 + rn * (e2 - 1.0); + } + if (!atPole) { + lla[1] = StrictMath.atan(sinP1/cosP1); + } + lla[0] = StrictMath.toDegrees(lla[0]); + lla[1] = StrictMath.toDegrees(lla[1]); + + return lla; + } + + /** + * simple even-odd point in polygon computation + * 1. Determine if point is contained in the longitudinal range + * 2. Determine whether point crosses the edge by computing the latitudinal delta + * between the end-point of a parallel vector (originating at the point) and the + * y-component of the edge sink + * + * NOTE: Requires polygon point (x,y) order either clockwise or counter-clockwise + */ + public static boolean pointInPolygon(double[] x, double[] y, double lat, double lon) { + assert x.length == y.length; + boolean inPoly = false; + /** + * Note: This is using a euclidean coordinate system which could result in + * upwards of 110KM error at the equator. + * TODO convert coordinates to cylindrical projection (e.g. mercator) + */ + for (int i = 1; i < x.length; i++) { + if (x[i] < lon && x[i-1] >= lon || x[i-1] < lon && x[i] >= lon) { + if (y[i] + (lon - x[i]) / (x[i-1] - x[i]) * (y[i-1] - y[i]) < lat) { + inPoly = !inPoly; + } + } + } + return inPoly; + } + + public static String geoTermToString(long term) { + StringBuilder s = new StringBuilder(64); + final int numberOfLeadingZeros = Long.numberOfLeadingZeros(term); + for (int i = 0; i < numberOfLeadingZeros; i++) { + s.append('0'); + } + if (term != 0) + s.append(Long.toBinaryString(term)); + return s.toString(); + } + + + public static boolean rectDisjoint(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY, + final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) { + return (aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY); + } + + /** + * Computes whether a rectangle is wholly within another rectangle (shared boundaries allowed) + */ + public static boolean rectWithin(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY, + final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) { + return !(aMinX < bMinX || aMinY < bMinY || aMaxX > bMaxX || aMaxY > bMaxY); + } + + public static boolean rectCrosses(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY, + final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) { + return !(rectDisjoint(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY) || + rectWithin(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY)); + } + + /** + * Computes whether rectangle a contains rectangle b (touching allowed) + */ + public static boolean rectContains(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY, + final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) { + return !(bMinX < aMinX || bMinY < aMinY || bMaxX > aMaxX || bMaxY > aMaxY); + } + + /** + * Computes whether a rectangle intersects another rectangle (crosses, within, touching, etc) + */ + public static boolean rectIntersects(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY, + final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) { + return !((aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY) ); + } + + /** + * Computes whether a rectangle crosses a shape. (touching not allowed) + */ + public static final boolean rectCrossesPoly(final double rMinX, final double rMinY, final double rMaxX, + final double rMaxY, final double[] shapeX, final double[] shapeY, + final double sMinX, final double sMinY, final double sMaxX, + final double sMaxY) { + // short-circuit: if the bounding boxes are disjoint then the shape does not cross + if (rectDisjoint(rMinX, rMinY, rMaxX, rMaxY, sMinX, sMinY, sMaxX, sMaxY)) + return false; + + final double[][] bbox = new double[][] { {rMinX, rMinY}, {rMaxX, rMinY}, {rMaxX, rMaxY}, {rMinX, rMaxY}, {rMinX, rMinY} }; + final int polyLength = shapeX.length-1; + double d, s, t, a1, b1, c1, a2, b2, c2; + double x00, y00, x01, y01, x10, y10, x11, y11; + + // computes the intersection point between each bbox edge and the polygon edge + for (short b=0; b<4; ++b) { + a1 = bbox[b+1][1]-bbox[b][1]; + b1 = bbox[b][0]-bbox[b+1][0]; + c1 = a1*bbox[b+1][0] + b1*bbox[b+1][1]; + for (int p=0; p s || x01 < s || y00 > t || y01 < t || x10 > s || x11 < s || y10 > t || y11 < t)) + return true; + } + } // for each poly edge + } // for each bbox edge + return false; + } + + /** + * Computes whether a rectangle is within a given polygon (shared boundaries allowed) + */ + public static boolean rectWithinPoly(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY, + final double[] shapeX, final double[] shapeY, final double sMinX, + final double sMinY, final double sMaxX, final double sMaxY) { + // check if rectangle crosses poly (to handle concave/pacman polys), then check that all 4 corners + // are contained + return !(rectCrossesPoly(rMinX, rMinY, rMaxX, rMaxY, shapeX, shapeY, sMinX, sMinY, sMaxX, sMaxY) || + !pointInPolygon(shapeX, shapeY, rMinY, rMinX) || !pointInPolygon(shapeX, shapeY, rMinY, rMaxX) || + !pointInPolygon(shapeX, shapeY, rMaxY, rMaxX) || !pointInPolygon(shapeX, shapeY, rMaxY, rMinX)); + } + + public static boolean isValidLat(double lat) { + return Double.isNaN(lat) == false && lat >= MIN_LAT_INCL && lat <= MAX_LAT_INCL; + } + + public static boolean isValidLon(double lon) { + return Double.isNaN(lon) == false && lon >= MIN_LON_INCL && lon <= MAX_LON_INCL; + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/util/package.html b/lucene/sandbox/src/java/org/apache/lucene/util/package.html new file mode 100644 index 00000000000..9a8856c38b2 --- /dev/null +++ b/lucene/sandbox/src/java/org/apache/lucene/util/package.html @@ -0,0 +1,28 @@ + + + + + + + + + + +This package contains utility APIs, currently for geospatial searching. + + diff --git a/lucene/sandbox/src/test/org/apache/lucene/search/TestGeoPointQuery.java b/lucene/sandbox/src/test/org/apache/lucene/search/TestGeoPointQuery.java new file mode 100644 index 00000000000..59cca0c385b --- /dev/null +++ b/lucene/sandbox/src/test/org/apache/lucene/search/TestGeoPointQuery.java @@ -0,0 +1,474 @@ +package org.apache.lucene.search; + +/* + * 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 java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import org.apache.lucene.analysis.MockAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.FieldType; +import org.apache.lucene.document.GeoPointField; +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.store.Directory; +import org.apache.lucene.util.FixedBitSet; +import org.apache.lucene.util.GeoUtils; +import org.apache.lucene.util.IOUtils; +import org.apache.lucene.util.LuceneTestCase; +import org.apache.lucene.util.TestUtil; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Unit testing for basic GeoPoint query logic + * + * @lucene.experimental + */ +public class TestGeoPointQuery extends LuceneTestCase { + private static Directory directory = null; + private static IndexReader reader = null; + private static IndexSearcher searcher = null; + + private static final String FIELD_NAME = "geoField"; + + // 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; + + @BeforeClass + public static void beforeClass() throws Exception { + directory = newDirectory(); + + // when we randomly test the full lat/lon space it can result in very very slow query times, this is due to the + // number of ranges that can be created in degenerate cases. + + // Between 1.0 and 3.0: + range = 2*(random().nextDouble() + 0.5); + originLon = GeoUtils.MIN_LON_INCL + range + (GeoUtils.MAX_LON_INCL - GeoUtils.MIN_LON_INCL - 2*range) * random().nextDouble(); + originLat = GeoUtils.MIN_LAT_INCL + range + (GeoUtils.MAX_LAT_INCL - GeoUtils.MIN_LAT_INCL - 2*range) * random().nextDouble(); + if (VERBOSE) { + System.out.println("TEST: originLon=" + originLon + " originLat=" + originLat + " range=" + range); + } + RandomIndexWriter writer = new RandomIndexWriter(random(), directory, + newIndexWriterConfig(new MockAnalyzer(random())) + .setMaxBufferedDocs(TestUtil.nextInt(random(), 100, 1000)) + .setMergePolicy(newLogMergePolicy())); + + // create some simple geo points + final FieldType storedPoint = new FieldType(GeoPointField.TYPE_STORED); + // this is a simple systematic test + GeoPointField[] pts = new GeoPointField[] { + new GeoPointField(FIELD_NAME, -96.4538113027811, 32.94823588839368, storedPoint), + new GeoPointField(FIELD_NAME, -96.7759895324707, 32.7559529921407, storedPoint), + new GeoPointField(FIELD_NAME, -96.77701950073242, 32.77866942010977, storedPoint), + new GeoPointField(FIELD_NAME, -96.7706036567688, 32.7756745755423, storedPoint), + new GeoPointField(FIELD_NAME, -139.73458170890808, 27.703618681345585, storedPoint), + new GeoPointField(FIELD_NAME, -96.65084838867188, 33.06047141970814, storedPoint), + new GeoPointField(FIELD_NAME, -96.7772, 32.778650, storedPoint), + new GeoPointField(FIELD_NAME, -83.99724648980559, 58.29438379542874, storedPoint), + new GeoPointField(FIELD_NAME, -26.779373834241003, 33.541429799076354, storedPoint), + new GeoPointField(FIELD_NAME, -77.35379276106497, 26.774024500421728, storedPoint), + new GeoPointField(FIELD_NAME, -14.796283808944777, -62.455081198245665, storedPoint)}; + + for (GeoPointField p : pts) { + Document doc = new Document(); + doc.add(p); + writer.addDocument(doc); + } + reader = writer.getReader(); + searcher = newSearcher(reader); + writer.close(); + } + + @AfterClass + public static void afterClass() throws Exception { + searcher = null; + reader.close(); + reader = null; + directory.close(); + directory = null; + } + + private TopDocs bboxQuery(double minLon, double minLat, double maxLon, double maxLat, int limit) throws Exception { + GeoPointInBBoxQuery q = new GeoPointInBBoxQuery(FIELD_NAME, minLon, minLat, maxLon, maxLat); + return searcher.search(q, limit); + } + + private TopDocs polygonQuery(double[] lon, double[] lat, int limit) throws Exception { + GeoPointInPolygonQuery q = new GeoPointInPolygonQuery(FIELD_NAME, lon, lat); + return searcher.search(q, limit); + } + + @Test + public void testBBoxQuery() throws Exception { + TopDocs td = bboxQuery(-96.7772, 32.778650, -96.77690000, 32.778950, 5); + assertEquals("GeoBoundingBoxQuery failed", 2, td.totalHits); + } + + @Test + public void testPolyQuery() throws Exception { + TopDocs td = polygonQuery( new double[] {-96.7682647, -96.8280029, -96.6288757, -96.4929199, + -96.6041564, -96.7449188, -96.76826477, -96.7682647}, + new double[] { 33.073130, 32.9942669, 32.938386, 33.0374494, + 33.1369762, 33.1162747, 33.073130, 33.073130}, 5); + assertEquals("GeoPolygonQuery failed", td.totalHits, 1); + } + + @Test + public void testPacManPolyQuery() throws Exception { + // pacman + double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0}; + double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0}; + + // shape bbox + double xMinA = -10; + double xMaxA = 10; + double yMinA = -10; + double yMaxA = 10; + + // candidate crosses cell + double xMin = 2;//-5; + double xMax = 11;//0.000001; + double yMin = -1;//0; + double yMax = 1;//5; + + // test cell crossing poly + assertTrue(GeoUtils.rectCrossesPoly(xMin, yMin, xMax, yMax, px, py, xMinA, yMinA, xMaxA, yMaxA)); + assertFalse(GeoUtils.rectCrossesPoly(-5, 0, 0.000001, 5, px, py, xMin, yMin, xMax, yMax)); + assertTrue(GeoUtils.rectWithinPoly(-5, 0, -2, 5, px, py, xMin, yMin, xMax, yMax)); + } + + public void testRandomTiny() throws Exception { + // Make sure single-leaf-node case is OK: + doTestRandom(10); + } + + public void testRandom() throws Exception { + doTestRandom(10000); + } + + @Nightly + public void testRandomBig() throws Exception { + doTestRandom(1000000); + } + + 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]; + + boolean haveRealDoc = false; + + for (int docID=0;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] = randomLon(); + if (VERBOSE) { + //System.out.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] = randomLat(); + lons[docID] = lons[oldDocID]; + if (VERBOSE) { + //System.out.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.out.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID] + " (same lat/lon as doc=" + oldDocID + ")"); + } + } + } else { + lats[docID] = randomLat(); + lons[docID] = randomLon(); + haveRealDoc = true; + if (VERBOSE) { + //System.out.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID]); + } + } + } + + verify(lats, lons); + } + + private static void verify(double[] lats, double[] lons) throws Exception { + IndexWriterConfig iwc = newIndexWriterConfig(); + Directory dir; + if (lats.length > 100000) { + dir = newFSDirectory(createTempDir("TestGeoPointQuery")); + } else { + dir = newDirectory(); + } + Set deleted = new HashSet<>(); + // RandomIndexWriter is too slow here: + IndexWriter w = new IndexWriter(dir, iwc); + for(int id=0;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); + } + IndexReader r = DirectoryReader.open(w, true); + w.close(); + + IndexSearcher s = newSearcher(r); + + // Make sure queries are thread safe: + int numThreads = TestUtil.nextInt(random(), 2, 5); + + List threads = new ArrayList<>(); + final int iters = atLeast(100); + + final CountDownLatch startingGun = new CountDownLatch(1); + + for(int i=0;i