/* * 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.geopoint.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.geo.Rectangle; import org.apache.lucene.geo.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#haversinMeters(double, double, double, double)}. * 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 { /** latitude value (in degrees) for query location */ protected final double centerLat; /** longitude value (in degrees) for query location */ protected final double centerLon; /** distance (in meters) from lat, lon center location */ protected final double radiusMeters; /** partial haversin computation */ protected final double sortKey; // we must check these before passing to superclass or circleToBBox, or users can get a strange exception! private static double checkRadius(double radiusMeters) { if (Double.isFinite(radiusMeters) == false || radiusMeters < 0) { throw new IllegalArgumentException("invalid radiusMeters " + radiusMeters); } return radiusMeters; } /** * Constructs a Query for all {@link org.apache.lucene.spatial.geopoint.document.GeoPointField} types within a * distance (in meters) from a given point **/ public GeoPointDistanceQuery(final String field, final double centerLat, final double centerLon, final double radiusMeters) { this(field, Rectangle.fromPointDistance(centerLat, centerLon, checkRadius(radiusMeters)), centerLat, centerLon, radiusMeters); } private GeoPointDistanceQuery(final String field, final Rectangle bbox, final double centerLat, final double centerLon, final double radiusMeters) { super(field, bbox.minLat, bbox.maxLat, bbox.minLon, bbox.maxLon); this.centerLat = centerLat; this.centerLon = centerLon; this.radiusMeters = radiusMeters; this.sortKey = GeoUtils.distanceQuerySortKey(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 Rectangle(minLat, maxLat, GeoUtils.MIN_LON_INCL, maxLon)); bqb.add(new BooleanClause(left, BooleanClause.Occur.SHOULD)); if (unwrappedLon < maxLon) { // unwrap right unwrappedLon += 360.0D; } GeoPointDistanceQueryImpl right = new GeoPointDistanceQueryImpl(field, this, unwrappedLon, new Rectangle(minLat, maxLat, minLon, GeoUtils.MAX_LON_INCL)); bqb.add(new BooleanClause(right, BooleanClause.Occur.SHOULD)); return bqb.build(); } return new GeoPointDistanceQueryImpl(field, this, centerLon, new Rectangle(this.minLat, this.maxLat, this.minLon, this.maxLon)); } @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(centerLat) .append(',') .append(centerLon) .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; } }