/*
* 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);
}
}
}