/*
 * 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.geo;

import static org.apache.lucene.util.SloppyMath.TO_RADIANS;
import static org.apache.lucene.util.SloppyMath.cos;
import static org.apache.lucene.util.SloppyMath.haversinMeters;

import org.apache.lucene.index.PointValues;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.util.SloppyMath;

Basic reusable geo-spatial utility methods
@lucene.experimental
/** * Basic reusable geo-spatial utility methods * * @lucene.experimental */
public final class GeoUtils {
Minimum longitude value.
/** Minimum longitude value. */
public static final double MIN_LON_INCL = -180.0D;
Maximum longitude value.
/** Maximum longitude value. */
public static final double MAX_LON_INCL = 180.0D;
Minimum latitude value.
/** Minimum latitude value. */
public static final double MIN_LAT_INCL = -90.0D;
Maximum latitude value.
/** Maximum latitude value. */
public static final double MAX_LAT_INCL = 90.0D;
min longitude value in radians
/** min longitude value in radians */
public static final double MIN_LON_RADIANS = TO_RADIANS * MIN_LON_INCL;
min latitude value in radians
/** min latitude value in radians */
public static final double MIN_LAT_RADIANS = TO_RADIANS * MIN_LAT_INCL;
max longitude value in radians
/** max longitude value in radians */
public static final double MAX_LON_RADIANS = TO_RADIANS * MAX_LON_INCL;
max latitude value in radians
/** max latitude value in radians */
public static final double MAX_LAT_RADIANS = TO_RADIANS * MAX_LAT_INCL; // WGS84 earth-ellipsoid parameters
mean earth axis in meters
/** mean earth axis in meters */
// see http://earth-info.nga.mil/GandG/publications/tr8350.2/wgs84fin.pdf public static final double EARTH_MEAN_RADIUS_METERS = 6_371_008.7714; // No instance: private GeoUtils() { }
validates latitude value is within standard +/-90 coordinate bounds
/** validates latitude value is within standard +/-90 coordinate bounds */
public static void checkLatitude(double latitude) { if (Double.isNaN(latitude) || latitude < MIN_LAT_INCL || latitude > MAX_LAT_INCL) { throw new IllegalArgumentException("invalid latitude " + latitude + "; must be between " + MIN_LAT_INCL + " and " + MAX_LAT_INCL); } }
validates longitude value is within standard +/-180 coordinate bounds
/** validates longitude value is within standard +/-180 coordinate bounds */
public static void checkLongitude(double longitude) { if (Double.isNaN(longitude) || longitude < MIN_LON_INCL || longitude > MAX_LON_INCL) { throw new IllegalArgumentException("invalid longitude " + longitude + "; must be between " + MIN_LON_INCL + " and " + MAX_LON_INCL); } } // some sloppyish stuff, do we really need this to be done in a sloppy way? // unless it is performance sensitive, we should try to remove. private static final double PIO2 = Math.PI / 2D;
Returns the trigonometric sine of an angle converted as a cos operation.

Note that this is not quite right... e.g. sin(0) != 0

Special cases:

  • If the argument is NaN or an infinity, then the result is NaN.
Params:
  • a – an angle, in radians.
See Also:
Returns:the sine of the argument.
/** * Returns the trigonometric sine of an angle converted as a cos operation. * <p> * Note that this is not quite right... e.g. sin(0) != 0 * <p> * Special cases: * <ul> * <li>If the argument is {@code NaN} or an infinity, then the result is {@code NaN}. * </ul> * @param a an angle, in radians. * @return the sine of the argument. * @see Math#sin(double) */
// TODO: deprecate/remove this? at least its no longer public. public static double sloppySin(double a) { return cos(a - PIO2); }
binary search to find the exact sortKey needed to match the specified radius any sort key lte this is a query match.
/** * binary search to find the exact sortKey needed to match the specified radius * any sort key lte this is a query match. */
public static double distanceQuerySortKey(double radius) { // effectively infinite if (radius >= haversinMeters(Double.MAX_VALUE)) { return haversinMeters(Double.MAX_VALUE); } // this is a search through non-negative long space only long lo = 0; long hi = Double.doubleToRawLongBits(Double.MAX_VALUE); while (lo <= hi) { long mid = (lo + hi) >>> 1; double sortKey = Double.longBitsToDouble(mid); double midRadius = haversinMeters(sortKey); if (midRadius == radius) { return sortKey; } else if (midRadius > radius) { hi = mid - 1; } else { lo = mid + 1; } } // not found: this is because a user can supply an arbitrary radius, one that we will never // calculate exactly via our haversin method. double ceil = Double.longBitsToDouble(lo); assert haversinMeters(ceil) > radius; return ceil; }
Compute the relation between the provided box and distance query. This only works for boxes that do not cross the dateline.
/** * Compute the relation between the provided box and distance query. * This only works for boxes that do not cross the dateline. */
public static PointValues.Relation relate( double minLat, double maxLat, double minLon, double maxLon, double lat, double lon, double distanceSortKey, double axisLat) { if (minLon > maxLon) { throw new IllegalArgumentException("Box crosses the dateline"); } if ((lon < minLon || lon > maxLon) && (axisLat + Rectangle.AXISLAT_ERROR < minLat || axisLat - Rectangle.AXISLAT_ERROR > maxLat)) { // circle not fully inside / crossing axis if (SloppyMath.haversinSortKey(lat, lon, minLat, minLon) > distanceSortKey && SloppyMath.haversinSortKey(lat, lon, minLat, maxLon) > distanceSortKey && SloppyMath.haversinSortKey(lat, lon, maxLat, minLon) > distanceSortKey && SloppyMath.haversinSortKey(lat, lon, maxLat, maxLon) > distanceSortKey) { // no points inside return Relation.CELL_OUTSIDE_QUERY; } } if (within90LonDegrees(lon, minLon, maxLon) && SloppyMath.haversinSortKey(lat, lon, minLat, minLon) <= distanceSortKey && SloppyMath.haversinSortKey(lat, lon, minLat, maxLon) <= distanceSortKey && SloppyMath.haversinSortKey(lat, lon, maxLat, minLon) <= distanceSortKey && SloppyMath.haversinSortKey(lat, lon, maxLat, maxLon) <= distanceSortKey) { // we are fully enclosed, collect everything within this subtree return Relation.CELL_INSIDE_QUERY; } return Relation.CELL_CROSSES_QUERY; }
Return whether all points of [minLon,maxLon] are within 90 degrees of lon.
/** Return whether all points of {@code [minLon,maxLon]} are within 90 degrees of {@code lon}. */
static boolean within90LonDegrees(double lon, double minLon, double maxLon) { if (maxLon <= lon - 180) { lon -= 360; } else if (minLon >= lon + 180) { lon += 360; } return maxLon - lon < 90 && lon - minLon < 90; }
Returns a positive value if points a, b, and c are arranged in counter-clockwise order, negative value if clockwise, zero if collinear.
/** * Returns a positive value if points a, b, and c are arranged in counter-clockwise order, * negative value if clockwise, zero if collinear. */
// see the "Orient2D" method described here: // http://www.cs.berkeley.edu/~jrs/meshpapers/robnotes.pdf // https://www.cs.cmu.edu/~quake/robust.html // Note that this one does not yet have the floating point tricks to be exact! public static int orient(double ax, double ay, double bx, double by, double cx, double cy) { double v1 = (bx - ax) * (cy - ay); double v2 = (cx - ax) * (by - ay); if (v1 > v2) { return 1; } else if (v1 < v2) { return -1; } else { return 0; } }
uses orient method to compute whether two line segments cross
/** uses orient method to compute whether two line segments cross */
public static boolean lineCrossesLine(double a1x, double a1y, double b1x, double b1y, double a2x, double a2y, double b2x, double b2y) { if (orient(a2x, a2y, b2x, b2y, a1x, a1y) * orient(a2x, a2y, b2x, b2y, b1x, b1y) < 0 && orient(a1x, a1y, b1x, b1y, a2x, a2y) * orient(a1x, a1y, b1x, b1y, b2x, b2y) < 0) { return true; } return false; }
uses orient method to compute whether two line segments cross; boundaries included - returning true for lines that terminate on each other. e.g., (plus sign) + == true, and (capital 't') T == true Use lineCrossesLine to exclude lines that terminate on each other from the truth table
/** uses orient method to compute whether two line segments cross; boundaries included - returning true for * lines that terminate on each other. * * e.g., (plus sign) + == true, and (capital 't') T == true * * Use {@link #lineCrossesLine} to exclude lines that terminate on each other from the truth table **/
public static boolean lineCrossesLineWithBoundary(double a1x, double a1y, double b1x, double b1y, double a2x, double a2y, double b2x, double b2y) { if (orient(a2x, a2y, b2x, b2y, a1x, a1y) * orient(a2x, a2y, b2x, b2y, b1x, b1y) <= 0 && orient(a1x, a1y, b1x, b1y, a2x, a2y) * orient(a1x, a1y, b1x, b1y, b2x, b2y) <= 0) { return true; } return false; }
used to define the orientation of 3 points -1 = Clockwise 0 = Colinear 1 = Counter-clockwise
/** * used to define the orientation of 3 points * -1 = Clockwise * 0 = Colinear * 1 = Counter-clockwise **/
public enum WindingOrder { CW(-1), COLINEAR(0), CCW(1); private final int sign; WindingOrder(int sign) { this.sign = sign; } public int sign() {return sign;} public static WindingOrder fromSign(final int sign) { if (sign == CW.sign) return CW; if (sign == COLINEAR.sign) return COLINEAR; if (sign == CCW.sign) return CCW; throw new IllegalArgumentException("Invalid WindingOrder sign: " + sign); } } }