blob: bbce004daf04a1f360c405bd4e3f7769b8cc1bb8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.spatial.search;
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.GeoBoundingBox;
import org.apache.lucene.spatial.util.GeoDistanceUtils;
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 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, GeoBoundingBox 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) {
// 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 GeoBoundingBox(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 GeoBoundingBox(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 GeoBoundingBox(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;
}
}