Calculating bounding box a certain distance away from a lat/long coordinate in Java - math

Given a coordinate (lat, long), I am trying to calculate a square bounding box that is a given distance (e.g. 50km) away from the coordinate. So as input I have lat, long and distance and as output I would like two coordinates; one being the south-west (bottom-left) corner and one being the north-east (top-right) corner. I have seen a couple of answers on here that try to address this question in Python, but I am looking for a Java implementation in particular.
Just to be clear, I intend on using the algorithm on Earth only and so I don't need to accommodate a variable radius.
It doesn't have to be hugely accurate (+/-20% is fine) and it'll only be used to calculate bounding boxes over small distances (no more than 150km). So I'm happy to sacrifice some accuracy for an efficient algorithm. Any help is much appreciated.
Edit: I should have been clearer, I really am after a square, not a circle. I understand that the distance between the center of a square and various points along the square's perimeter is not a constant value like it is with a circle. I guess what I mean is a square where if you draw a line from the center to any one of the four points on the perimeter that results in a line perpendicular to a side of the perimeter, then those 4 lines have the same length.

I wrote an article about finding the bounding coordinates:
http://JanMatuschek.de/LatitudeLongitudeBoundingCoordinates
The article explains the formulae and also provides a Java implementation. (It also shows why IronMan's formula for the min/max longitude is inaccurate.)

double R = 6371; // earth radius in km
double radius = 50; // km
double x1 = lon - Math.toDegrees(radius/R/Math.cos(Math.toRadians(lat)));
double x2 = lon + Math.toDegrees(radius/R/Math.cos(Math.toRadians(lat)));
double y1 = lat + Math.toDegrees(radius/R);
double y2 = lat - Math.toDegrees(radius/R);
Although I would also recommend JTS.

import com.vividsolutions.jts.geom.Envelope;
...
Envelope env = new Envelope(centerPoint.getCoordinate());
env.expandBy(distance_in_degrees);
...
Now env contains your envelope. It's not actually a "square" (whatever that means on the surface of a sphere), but it should do.
You should note that the distance in degrees will depend on the latitude of the center point. At the equator, 1 degree of latitude is about 111km, but in New York, it's only about 75km.
The really cool thing is that you can toss all your points into a com.vividsolutions.jts.index.strtree.STRtree and then use it to quickly calculate points inside that Envelope.

All of the previous answers are only partially correct. Specially in region like Australia, they always include pole and calculate a very large rectangle even for 10kms.
Specially the algorithm by Jan Philip Matuschek at http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates#UsingIndex included a very large rectangle from (-37, -90, -180, 180) for almost every point in Australia. This hits a large users in database and distance have to be calculated for all of the users in almost half the country.
I found that the Drupal API Earth Algorithm by Rochester Institute of Technology works better around pole as well as elsewhere and is much easier to implement.
https://www.rit.edu/drupal/api/drupal/sites%21all%21modules%21location%21earth.inc/7.54
Use earth_latitude_range and earth_longitude_range from the above algorithm for calculating bounding rectangle
Here is the implementation is Java
/**
* Get bouding rectangle using Drupal Earth Algorithm
* #see https://www.rit.edu/drupal/api/drupal/sites%21all%21modules%21location%21earth.inc/7.54
* #param lat
* #param lng
* #param distance
* #return
*/
default BoundingRectangle getBoundingRectangleDrupalEarthAlgo(double lat, double lng, int distance) {
lng = Math.toRadians(lng);
lat = Math.toRadians(lat);
double radius = earth_radius(lat);
List<Double> retLats = earth_latitude_range(lat, radius, distance);
List<Double> retLngs = earth_longitude_range(lat, lng, radius, distance);
return new BoundingRectangle(retLats.get(0), retLats.get(1), retLngs.get(0), retLngs.get(1));
}
/**
* Calculate latitude range based on earths radius at a given point
* #param latitude
* #param longitude
* #param distance
* #return
*/
default List<Double> earth_latitude_range(double lat, double radius, double distance) {
// Estimate the min and max latitudes within distance of a given location.
double angle = distance / radius;
double minlat = lat - angle;
double maxlat = lat + angle;
double rightangle = Math.PI / 2;
// Wrapped around the south pole.
if (minlat < -rightangle) {
double overshoot = -minlat - rightangle;
minlat = -rightangle + overshoot;
if (minlat > maxlat) {
maxlat = minlat;
}
minlat = -rightangle;
}
// Wrapped around the north pole.
if (maxlat > rightangle) {
double overshoot = maxlat - rightangle;
maxlat = rightangle - overshoot;
if (maxlat < minlat) {
minlat = maxlat;
}
maxlat = rightangle;
}
List<Double> ret = new ArrayList<>();
ret.add((minlat));
ret.add((maxlat));
return ret;
}
/**
* Calculate longitude range based on earths radius at a given point
* #param lat
* #param lng
* #param earth_radius
* #param distance
* #return
*/
default List<Double> earth_longitude_range(double lat, double lng, double earth_radius, int distance) {
// Estimate the min and max longitudes within distance of a given location.
double radius = earth_radius * Math.cos(lat);
double angle;
if (radius > 0) {
angle = Math.abs(distance / radius);
angle = Math.min(angle, Math.PI);
}
else {
angle = Math.PI;
}
double minlong = lng - angle;
double maxlong = lng + angle;
if (minlong < -Math.PI) {
minlong = minlong + Math.PI * 2;
}
if (maxlong > Math.PI) {
maxlong = maxlong - Math.PI * 2;
}
List<Double> ret = new ArrayList<>();
ret.add((minlong));
ret.add((maxlong));
return ret;
}
/**
* Calculate earth radius at given latitude
* #param latitude
* #return
*/
default Double earth_radius(double latitude) {
// Estimate the Earth's radius at a given latitude.
// Default to an approximate average radius for the United States.
double lat = Math.toRadians(latitude);
double x = Math.cos(lat) / 6378137.0;
double y = Math.sin(lat) / (6378137.0 * (1 - (1 / 298.257223563)));
//Make sure earth's radius is in km , not meters
return (1 / (Math.sqrt(x * x + y * y)))/1000;
}
And use the distance calculation formula documented by google maps to calculate distance
https://developers.google.com/maps/solutions/store-locator/clothing-store-locator#outputting-data-as-xml-using-php
To search by kilometers instead of miles, replace 3959 with 6371.
For (Lat, Lng) = (37, -122) and a Markers table with columns lat and lng, the formula is:
SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS distance FROM markers HAVING distance < 25 ORDER BY distance LIMIT 0 , 20;

Based on IronMan response:
/**
* Calculate the lat and len of a square around a point.
* #return latMin, latMax, lngMin, lngMax
*/
public static double[] calculateSquareRadius(double lat, double lng, double radius) {
double R = 6371; // earth radius in km
double latMin = lat - Math.toDegrees(radius/R);
double latMax = lat + Math.toDegrees(radius/R);
double lngMin = lng - Math.toDegrees(radius/R/Math.cos(Math.toRadians(lat)));
double lngMax = lng + Math.toDegrees(radius/R/Math.cos(Math.toRadians(lat)));
return new double[] {latMin, latMax, lngMin, lngMax};
}

Here is a simple solution that I used to generate bounding box coordinates that I use with GeoNames citieJSON API to get nearby big cities from a gps decimal coordinate.
This is a Java method from my GitHub repository: FusionTableModifyJava
I had a decimal GPS location and I needed to find the biggest city/state "near" that location. I needed a relatively accurate bounding box to pass to the citiesJSON GeoNames webservice to get back the biggest city in that bounding box. I pass the location and the "radius" I am interested in (in km) and it gives back the north, south, east, west decimal coordinates needed to pass to citiesJSON.
(I found these resources useful in doing my research:
Calculate distance, bearing and more between Latitude/Longitude points.
Longitude - Wikipedia)
It is not super accurate but accurate enough for what I was using it for:
// Compute bounding Box coordinates for use with Geonames API.
class BoundingBox
{
public double north, south, east, west;
public BoundingBox(String location, float km)
{
//System.out.println(location + " : "+ km);
String[] parts = location.replaceAll("\\s","").split(","); //remove spaces and split on ,
double lat = Double.parseDouble(parts[0]);
double lng = Double.parseDouble(parts[1]);
double adjust = .008983112; // 1km in degrees at equator.
//adjust = 0.008983152770714983; // 1km in degrees at equator.
//System.out.println("deg: "+(1.0/40075.017)*360.0);
north = lat + ( km * adjust);
south = lat - ( km * adjust);
double lngRatio = 1/Math.cos(Math.toRadians(lat)); //ratio for lng size
//System.out.println("lngRatio: "+lngRatio);
east = lng + (km * adjust) * lngRatio;
west = lng - (km * adjust) * lngRatio;
}
}

Related

Lat/Lng to Web Mercator Projection Issues

I've got some simple code in a Qt application which converts from a latitude/longitude coordinate to a web mercator x/y position:
// Radius of the Earth in metres
const double EARTH_RADIUS = 20037508.34;
// Convert from a LL to a QPointF
QPointF GeoPoint::toMercator() {
double x = this->longitude() * EARTH_RADIUS / 180.0;
double y = log(tan((90.0 + this->latitude()) * PI / 360.0)) / (PI / 180.0);
y = y * EARTH_RADIUS / 180.0;
return QPointF(x, y);
}
// Convert from a QPointF to a LL
GeoPoint GeoPoint::fromMercator(const QPointF &pt) {
double lon = (pt.x() / EARTH_RADIUS) * 180.0;
double lat = (pt.y() / EARTH_RADIUS) * 180.0;
lat = 180.0 / PI * (2 * atan(exp(lat * PI / 180.0)) - PI / 2.0);
return GeoPoint(lat, lon);
}
I'm wanting to get the geographic position of a number of objects which are in metres distance away from a geographic origin, however either my lack of understanding or source code are not correct.
Consider the following:
GeoPoint pt1(54.253230, -3.006460);
QPointF m1 = pt1.toMercator();
qDebug() << m1;
// QPointF(-334678,7.21826e+06)
// Now I want to add a distance onto the mercator coordinates, i.e. 50 metres
m1.rx() += 50.0;
qDebug() << m1;
// QPointF(-334628,7.21826e+06)
// Take this back to a LL
GeoPoint pt1a = GeoPoint::fromMercator(m1);
qDebug() << pt1a.toString();
// "54.25323°, -3.00601°"
If I plot the two LL coordinates into Google Earth, they are not 50m apart as expected, they are about 29.3m apart.
I'm perplexed!

Calculate min distance between a "line" and one "point"

I have a "linestring" (with init and end points) and a single "point" (two coordinates).
And I have implemented the following ActionSctipt code to use "haversine formula" to calculate the distance between two points (each point has x & y coordinates); this function can return the "distance" in "kms", "meters", "feets" or "miles":
private function distanceBetweenCoordinates(lat1:Number, lon1:Number, lat2:Number, lon2:Number, units:String = "miles"):Number {
var R:int = RADIUS_OF_EARTH_IN_MILES;
if (units == "km") {
R = RADIUS_OF_EARTH_IN_KM;
}
if (units == "meters") {
R = RADIUS_OF_EARTH_IN_M;
}
if (units == "feet") {
R = RADIUS_OF_EARTH_IN_FEET;
}
var dLat:Number = (lat2 - lat1) * Math.PI / 180;
var dLon:Number = (lon2 - lon1) * Math.PI / 180;
var lat1inRadians:Number = lat1 * Math.PI / 180;
var lat2inRadians:Number = lat2 * Math.PI / 180;
var a:Number = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1inRadians) * Math.cos(lat2inRadians);
var c:Number = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d:Number = R * c;
return d;
}
This code is functioning well. But I need to improving this code to allow calculate the minimum distance between a "single point" and one "linestring" (with 2 points).
How can I do?
I thought this solution:
* Divide the "linesting" for each point (Init and end)... and for each of these calculate the distance to the "single point"... and after I getting both "distances" return the minimum distance.
This solution is not the better, this is explained in the following image:
"d1" and "d2" distances are invalid... because only "d0" is the valid distance.
Please! help me!!! How can I improve the haversine formula to calculate the distance between a line and a single point in kilometres?
Thanks!!!!
In your case d0 distance is a height of triangle. It's Hb=2*A/b where A- Area & b-length of the base side (your linestring).
If given 3 points you can calculate the the distances between them (sides a, b, c of triangle). It will allow you to calculate triangle Area: A=sqrt(p*(p-a)*(p-b)*(p-c)) where p is half perimeter: p=(a+b+c)/2. So, now u have all variables u need to calculate the distance Hb (your "d0").

draw flighroute on mercator map how to calculate polyline points with a given longitude

i would like to draw a polyline on a Mercator map between two cities. e.g Startpoints:
Location berlin = new Location(52.517, 13.40);
Location tokio = new Location(35.70,139.767);
it should look like a flight route.
so my plan was to go through all longitude values between the two cities and calculate corresponding latitude values:
LocationCollection locationCollection = new LocationCollection();
Location next = new Location(berlin.Latitude,berlin.Longitude); //startpunkt
for (double x = berlin.Longitude+1; x < tokio.Longitude; x++) {
locationCollection.Add(next);
next = new Location(???, x);
}
the question is how can i calculate the latitude for each longitude value for the polyline?
Thanks!
From this link:
Here's an implementation in C.
#define PI 3.14159265358979323846
double degrees_radians(double d) { return d * PI / 180.0; }
double radians_degrees(double r) { return r * 180.0 / PI; }
double latitude(double lat1, double lon1, double lat2, double lon2, double lon)
{
return atan((sin(lat1)*cos(lat2)*sin(lon-lon2)-sin(lat2)*cos(lat1)*sin(lon-lon1))/(cos(lat1)*cos(lat2)*sin(lon1-lon2)));
}
int main()
{
// start and end in degrees
double lat1_d = 52.517;
double lon1_d = 13.40;
double lat2_d = 35.70;
double lon2_d = 139.767;
// start and end in radians
double lat1_r = degrees_radians(lat1_d);
double lon1_r = degrees_radians(lon1_d);
double lat2_r = degrees_radians(lat2_d);
double lon2_r = degrees_radians(lon2_d);
// interpolate latitide at every degree of longitude between 1 and 2
for (double lon = ceil(lon1_d); lon < lon2_d; lon += 1.0)
{
double lat_r = latitude(lat1_r, lon1_r, lat2_r, lon2_r, degrees_radians(lon));
double lat_d = radians_degrees(lat_r);
printf("%.3f , %.3f\n", lat_d, lon);
}
return 0;
}
The maximum latitude reached on the great circle between Berlin and Tokyo is then shown to be 66.183° at longitude 68°.

Get direction (compass) with two longitude/latitude points

I'm working on a "compass" for a mobile-device. I have the following points:
point 1 (current location): Latitude = 47.2246, Longitude = 8.8257
point 2 (target location): Latitude = 50.9246, Longitude = 10.2257
Also I have the following information (from my android-phone):
The compass-direction in degree, which bears to the north.
For example, when I direct my phone to north, I get 0°
How can I create a "compass-like" arrow which shows me the direction to the point?
Is there a mathematic-problem for this?
EDIT: Okay I found a solution, it looks like this:
/**
* Params: lat1, long1 => Latitude and Longitude of current point
* lat2, long2 => Latitude and Longitude of target point
*
* headX => x-Value of built-in phone-compass
*
* Returns the degree of a direction from current point to target point
*
*/
function getDegrees(lat1, long1, lat2, long2, headX) {
var dLat = toRad(lat2-lat1);
var dLon = toRad(lon2-lon1);
lat1 = toRad(lat1);
lat2 = toRad(lat2);
var y = Math.sin(dLon) * Math.cos(lat2);
var x = Math.cos(lat1)*Math.sin(lat2) -
Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
var brng = toDeg(Math.atan2(y, x));
// fix negative degrees
if(brng<0) {
brng=360-Math.abs(brng);
}
return brng - headX;
}
O forgot to say I found the answer eventually. The application is to determine compass direction of a transit vehicle and its destination. Essentially, fancy math for acquiring curvature of Earth, finding an angle/compass reading, and then matching that angle with a generic compass value. You could of course just keep the compassReading and apply that as an amount of rotation for your image. Please note this is an averaged determination of the vehicle direction to the end point (bus station) meaning it can't know what the road is doing (so this probably best applies to airplanes or roller derby).
//example obj data containing lat and lng points
//stop location - the radii end point
endpoint.lat = 44.9631;
endpoint.lng = -93.2492;
//bus location from the southeast - the circle center
startpoint.lat = 44.95517;
startpoint.lng = -93.2427;
function vehicleBearing(endpoint, startpoint) {
endpoint.lat = x1;
endpoint.lng = y1;
startpoint.lat = x2;
startpoint.lng = y2;
var radians = getAtan2((y1 - y2), (x1 - x2));
function getAtan2(y, x) {
return Math.atan2(y, x);
};
var compassReading = radians * (180 / Math.PI);
var coordNames = ["N", "NE", "E", "SE", "S", "SW", "W", "NW", "N"];
var coordIndex = Math.round(compassReading / 45);
if (coordIndex < 0) {
coordIndex = coordIndex + 8
};
return coordNames[coordIndex]; // returns the coordinate value
}
ie:
vehicleBearing(mybus, busstation)
might return "NW" means its travelling northwesterly
I found some useful gps coordinates formula in math here.
For this case, here my solution
private double getDirection(double lat1, double lng1, double lat2, double lng2) {
double PI = Math.PI;
double dTeta = Math.log(Math.tan((lat2/2)+(PI/4))/Math.tan((lat1/2)+(PI/4)));
double dLon = Math.abs(lng1-lng2);
double teta = Math.atan2(dLon,dTeta);
double direction = Math.round(Math.toDegrees(teta));
return direction; //direction in degree
}
I couldn't understand your solution well, calculating the slope worked for me.
To modify on efwjames's and your answer. This should do -
import math
def getDegrees(lat1, lon1, lat2, lon2,head):
dLat = math.radians(lat2-lat1)
dLon = math.radians(lon2-lon1)
bearing = math.degrees(math.atan2(dLon, dLat))
return head-bearing
You'd need to calculate an Euclidean vector between your start point and end point, then calculate its angle (let's say relative to positive X) which would be the angle you want to rotate your arrow by.

Detecting whether a GPS coordinate falls within a polygon on a map

As stated in the title, the goal is to have a way for detecting whether a given GPS coordinate falls inside a polygon or not.
The polygon itself can be either convex or concave. It's defined as a set of edge vectors and a known point within that polygon. Each edge vector is further defined by four coordinates which are the latitudes and longitudes of respective tip points and a bearing relative to the starting point.
There are a couple of questions similar to this one here on StackOverflow but they describe the solution only in general terms and for a 2D plane, whereas I am looking for an existing implementation that supports polygons defined by latitude/longitude pairs in WGS 84.
What API-s or services are out there for doing such collision tests?
Here is a java program which uses a function that will return true if a latitude/longitude is found inside of a polygon defined by a list of lat/longs, with demonstration for the state of florida.
I'm not sure if it deals with the fact that the lat/long GPS system is not an x/y coordinate plane. For my uses I have demonstrated that it works (I think if you specify enough points in the bounding box, it washes away the effect that the earth is a sphere, and that straight lines between two points on the earth is not an arrow straight line.
First specify the points that make up the corner points of the polygon, it can have concave and convex corners. The coordinates I use below traces the perimeter of the state of Florida.
method coordinate_is_inside_polygon utilizes an algorithm I don't quite understand. Here is an official explanation from the source where I got it:
"... solution forwarded by Philippe Reverdy is to compute the sum of the angles made between the test point and each pair of points making up the polygon. If this sum is 2pi then the point is an interior point, if 0 then the point is an exterior point. This also works for polygons with holes given the polygon is defined with a path made up of coincident edges into and out of the hole as is common practice in many CAD packages. "
My unit tests show it does work reliably, even when the bounding box is a 'C' shape or even shaped like a Torus. (My unit tests test many points inside Florida and make sure the function returns true. And I pick a number of coordinates everywhere else in the world and make sure it returns false. I pick places all over the world which might confuse it.
I'm not sure this will work if the polygon bounding box crosses the equator, prime meridian, or any area where the coordinates change from -180 -> 180, -90 -> 90. Or your polygon wraps around the earth around the north/south poles. For me, I only need it to work for the perimeter of Florida. If you have to define a polygon that spans the earth or crosses these lines, you could work around it by making two polygons, one representing the area on one side of the meridian and one representing the area on the other side and testing if your point is in either of those points.
Here is where I found this algorithm: Determining if a point lies on the interior of a polygon - Solution 2
Run it for yourself to double check it.
Put this in a file called Runner.java
import java.util.ArrayList;
public class Runner
{
public static double PI = 3.14159265;
public static double TWOPI = 2*PI;
public static void main(String[] args) {
ArrayList<Double> lat_array = new ArrayList<Double>();
ArrayList<Double> long_array = new ArrayList<Double>();
//This is the polygon bounding box, if you plot it,
//you'll notice it is a rough tracing of the parameter of
//the state of Florida starting at the upper left, moving
//clockwise, and finishing at the upper left corner of florida.
ArrayList<String> polygon_lat_long_pairs = new ArrayList<String>();
polygon_lat_long_pairs.add("31.000213,-87.584839");
//lat/long of upper left tip of florida.
polygon_lat_long_pairs.add("31.009629,-85.003052");
polygon_lat_long_pairs.add("30.726726,-84.838257");
polygon_lat_long_pairs.add("30.584962,-82.168579");
polygon_lat_long_pairs.add("30.73617,-81.476441");
//lat/long of upper right tip of florida.
polygon_lat_long_pairs.add("29.002375,-80.795288");
polygon_lat_long_pairs.add("26.896598,-79.938355");
polygon_lat_long_pairs.add("25.813738,-80.059204");
polygon_lat_long_pairs.add("24.93028,-80.454712");
polygon_lat_long_pairs.add("24.401135,-81.817017");
polygon_lat_long_pairs.add("24.700927,-81.959839");
polygon_lat_long_pairs.add("24.950203,-81.124878");
polygon_lat_long_pairs.add("26.0015,-82.014771");
polygon_lat_long_pairs.add("27.833247,-83.014527");
polygon_lat_long_pairs.add("28.8389,-82.871704");
polygon_lat_long_pairs.add("29.987293,-84.091187");
polygon_lat_long_pairs.add("29.539053,-85.134888");
polygon_lat_long_pairs.add("30.272352,-86.47522");
polygon_lat_long_pairs.add("30.281839,-87.628784");
//Convert the strings to doubles.
for(String s : polygon_lat_long_pairs){
lat_array.add(Double.parseDouble(s.split(",")[0]));
long_array.add(Double.parseDouble(s.split(",")[1]));
}
//prints TRUE true because the lat/long passed in is
//inside the bounding box.
System.out.println(coordinate_is_inside_polygon(
25.7814014D,-80.186969D,
lat_array, long_array));
//prints FALSE because the lat/long passed in
//is Not inside the bounding box.
System.out.println(coordinate_is_inside_polygon(
25.831538D,-1.069338D,
lat_array, long_array));
}
public static boolean coordinate_is_inside_polygon(
double latitude, double longitude,
ArrayList<Double> lat_array, ArrayList<Double> long_array)
{
int i;
double angle=0;
double point1_lat;
double point1_long;
double point2_lat;
double point2_long;
int n = lat_array.size();
for (i=0;i<n;i++) {
point1_lat = lat_array.get(i) - latitude;
point1_long = long_array.get(i) - longitude;
point2_lat = lat_array.get((i+1)%n) - latitude;
//you should have paid more attention in high school geometry.
point2_long = long_array.get((i+1)%n) - longitude;
angle += Angle2D(point1_lat,point1_long,point2_lat,point2_long);
}
if (Math.abs(angle) < PI)
return false;
else
return true;
}
public static double Angle2D(double y1, double x1, double y2, double x2)
{
double dtheta,theta1,theta2;
theta1 = Math.atan2(y1,x1);
theta2 = Math.atan2(y2,x2);
dtheta = theta2 - theta1;
while (dtheta > PI)
dtheta -= TWOPI;
while (dtheta < -PI)
dtheta += TWOPI;
return(dtheta);
}
public static boolean is_valid_gps_coordinate(double latitude,
double longitude)
{
//This is a bonus function, it's unused, to reject invalid lat/longs.
if (latitude > -90 && latitude < 90 &&
longitude > -180 && longitude < 180)
{
return true;
}
return false;
}
}
Demon magic needs to be unit-tested. Put this in a file called MainTest.java to verify it works for you
import java.util.ArrayList;
import org.junit.Test;
import static org.junit.Assert.*;
public class MainTest {
#Test
public void test_lat_long_in_bounds(){
Runner r = new Runner();
//These make sure the lat/long passed in is a valid gps
//lat/long coordinate. These should be valid.
assertTrue(r.is_valid_gps_coordinate(25, -82));
assertTrue(r.is_valid_gps_coordinate(-25, -82));
assertTrue(r.is_valid_gps_coordinate(25, 82));
assertTrue(r.is_valid_gps_coordinate(-25, 82));
assertTrue(r.is_valid_gps_coordinate(0, 0));
assertTrue(r.is_valid_gps_coordinate(89, 179));
assertTrue(r.is_valid_gps_coordinate(-89, -179));
assertTrue(r.is_valid_gps_coordinate(89.999, 179));
//If your bounding box crosses the equator or prime meridian,
then you have to test for those situations still work.
}
#Test
public void realTest_for_points_inside()
{
ArrayList<Double> lat_array = new ArrayList<Double>();
ArrayList<Double> long_array = new ArrayList<Double>();
ArrayList<String> polygon_lat_long_pairs = new ArrayList<String>();
//upper left tip of florida.
polygon_lat_long_pairs.add("31.000213,-87.584839");
polygon_lat_long_pairs.add("31.009629,-85.003052");
polygon_lat_long_pairs.add("30.726726,-84.838257");
polygon_lat_long_pairs.add("30.584962,-82.168579");
polygon_lat_long_pairs.add("30.73617,-81.476441");
//upper right tip of florida.
polygon_lat_long_pairs.add("29.002375,-80.795288");
polygon_lat_long_pairs.add("26.896598,-79.938355");
polygon_lat_long_pairs.add("25.813738,-80.059204");
polygon_lat_long_pairs.add("24.93028,-80.454712");
polygon_lat_long_pairs.add("24.401135,-81.817017");
polygon_lat_long_pairs.add("24.700927,-81.959839");
polygon_lat_long_pairs.add("24.950203,-81.124878");
polygon_lat_long_pairs.add("26.0015,-82.014771");
polygon_lat_long_pairs.add("27.833247,-83.014527");
polygon_lat_long_pairs.add("28.8389,-82.871704");
polygon_lat_long_pairs.add("29.987293,-84.091187");
polygon_lat_long_pairs.add("29.539053,-85.134888");
polygon_lat_long_pairs.add("30.272352,-86.47522");
polygon_lat_long_pairs.add("30.281839,-87.628784");
for(String s : polygon_lat_long_pairs){
lat_array.add(Double.parseDouble(s.split(",")[0]));
long_array.add(Double.parseDouble(s.split(",")[1]));
}
Runner r = new Runner();
ArrayList<String> pointsInside = new ArrayList<String>();
pointsInside.add("30.82112,-87.255249");
pointsInside.add("30.499804,-86.8927");
pointsInside.add("29.96826,-85.036011");
pointsInside.add("30.490338,-83.981323");
pointsInside.add("29.825395,-83.344116");
pointsInside.add("30.215406,-81.828003");
pointsInside.add("29.299813,-82.728882");
pointsInside.add("28.540135,-81.212769");
pointsInside.add("27.92065,-82.619019");
pointsInside.add("28.143691,-81.740113");
pointsInside.add("27.473186,-80.718384");
pointsInside.add("26.769154,-81.729126");
pointsInside.add("25.853292,-80.223999");
pointsInside.add("25.278477,-80.707398");
pointsInside.add("24.571105,-81.762085"); //bottom tip of keywest
pointsInside.add("24.900388,-80.663452");
pointsInside.add("24.680963,-81.366577");
for(String s : pointsInside)
{
assertTrue(r.coordinate_is_inside_polygon(
Double.parseDouble(s.split(",")[0]),
Double.parseDouble(s.split(",")[1]),
lat_array, long_array));
}
}
#Test
public void realTest_for_points_outside()
{
ArrayList<Double> lat_array = new ArrayList<Double>();
ArrayList<Double> long_array = new ArrayList<Double>();
ArrayList<String> polygon_lat_long_pairs = new ArrayList<String>();
//upper left tip, florida.
polygon_lat_long_pairs.add("31.000213,-87.584839");
polygon_lat_long_pairs.add("31.009629,-85.003052");
polygon_lat_long_pairs.add("30.726726,-84.838257");
polygon_lat_long_pairs.add("30.584962,-82.168579");
polygon_lat_long_pairs.add("30.73617,-81.476441");
//upper right tip, florida.
polygon_lat_long_pairs.add("29.002375,-80.795288");
polygon_lat_long_pairs.add("26.896598,-79.938355");
polygon_lat_long_pairs.add("25.813738,-80.059204");
polygon_lat_long_pairs.add("24.93028,-80.454712");
polygon_lat_long_pairs.add("24.401135,-81.817017");
polygon_lat_long_pairs.add("24.700927,-81.959839");
polygon_lat_long_pairs.add("24.950203,-81.124878");
polygon_lat_long_pairs.add("26.0015,-82.014771");
polygon_lat_long_pairs.add("27.833247,-83.014527");
polygon_lat_long_pairs.add("28.8389,-82.871704");
polygon_lat_long_pairs.add("29.987293,-84.091187");
polygon_lat_long_pairs.add("29.539053,-85.134888");
polygon_lat_long_pairs.add("30.272352,-86.47522");
polygon_lat_long_pairs.add("30.281839,-87.628784");
for(String s : polygon_lat_long_pairs)
{
lat_array.add(Double.parseDouble(s.split(",")[0]));
long_array.add(Double.parseDouble(s.split(",")[1]));
}
Runner r = new Runner();
ArrayList<String> pointsOutside = new ArrayList<String>();
pointsOutside.add("31.451159,-87.958374");
pointsOutside.add("31.319856,-84.607544");
pointsOutside.add("30.868282,-84.717407");
pointsOutside.add("31.338624,-81.685181");
pointsOutside.add("29.452991,-80.498657");
pointsOutside.add("26.935783,-79.487915");
pointsOutside.add("25.159207,-79.916382");
pointsOutside.add("24.311058,-81.17981");
pointsOutside.add("25.149263,-81.838989");
pointsOutside.add("27.726326,-83.695679");
pointsOutside.add("29.787263,-87.024536");
pointsOutside.add("29.205877,-62.102052");
pointsOutside.add("14.025751,-80.690919");
pointsOutside.add("29.029276,-90.805666");
pointsOutside.add("-12.606032,-70.151369");
pointsOutside.add("-56.520716,-172.822269");
pointsOutside.add("-75.89666,9.082024");
pointsOutside.add("-24.078567,142.675774");
pointsOutside.add("84.940737,177.480462");
pointsOutside.add("47.374545,9.082024");
pointsOutside.add("25.831538,-1.069338");
pointsOutside.add("0,0");
for(String s : pointsOutside){
assertFalse(r.coordinate_is_inside_polygon(
Double.parseDouble(s.split(",")[0]),
Double.parseDouble(s.split(",")[1]), lat_array, long_array));
}
}
}
//The list of lat/long inside florida bounding box all return true.
//The list of lat/long outside florida bounding box all return false.
I used eclipse IDE to get this to run java using java 1.6.0. For me all the unit tests pass. You need to include the junit 4 jar file in your classpath or import it into Eclipse.
I thought similarly as shab first (his proposal is called Ray-Casting Algorithm), but had second thoughts like Spacedman:
...but all the geometry will have to be redone in spherical coordinates...
I implemented and tested the mathematically correct way of doing that, e.i. intersecting great circles and determining whether one of the two intersecting points is on both arcs. (Note: I followed the steps described here, but I found several errors: The sign function is missing at the end of step 6 (just before arcsin), and the final test is numerical garbage (as subtraction is badly conditioned); use rather L_1T >= max(L_1a, L_1b) to test whether S1 is on the first arc etc.)
That also is extremely slow and a numerical nightmare (evaluates ~100 trigonometric functions, among other things); it proved not to be usable in our embedded systems.
There's a trick, though: If the area you are considering is small enough, just do a standard cartographic projection, e.g. spherical Mercator projection, of each point:
// latitude, longitude in radians
x = longitude;
y = log(tan(pi/4 + latitude/2));
Then, you can apply ray-casting, where the intersection of arcs is checked by this function:
public bool ArcsIntersecting(double x1, double y1, double x2, double y2,
double x3, double y3, double x4, double y4)
{
double vx1 = x2 - x1;
double vy1 = y2 - y1;
double vx2 = x4 - x3;
double vy2 = y4 - y3;
double denom = vx1 * vy2 - vx2 * vy1;
if (denom == 0) { return false; } // edges are parallel
double t1 = (vx2 * (y1 - y3) - vy2 * (x1 - x3)) / denom;
double t2;
if (vx2 != 0) { t2 = (x1 - x3 + t1 * vx1) / vx2; }
else if (vy2 != 0) { t2 = (y1 - y3 + t1 * vy1) / vy2; }
else { return false; } // edges are matching
return min(t1, t2) >= 0 && max(t1, t2) <= 1;
}
If you have WGS84 coordinates on the sphere, then your polygon divides the sphere into two areas - how do we know which area is 'inside' and which is 'outside' the polygon? The question is essentially meaningless!
For example, suppose the polygon formed the line of the equator - is the Northern hemisphere 'in' or 'out'?
From memory, the way to determine whether a point lies within a polygon is to imagine drawing a line from the position to some far away point. You then count the number of intersections between the line and the line segments of the polygon. If it count is even, then it does not lie within the polygon. If it is false, then it does lie within the polygon.
JavaScript Version -
{
const PI = 3.14159265;
const TWOPI = 2*PI;
function isCoordinateInsidePitch(latitude, longitude, latArray, longArray)
{
let angle=0;
let p1Lat;
let p1Long;
let p2Lat;
let p2Long;
let n = latArray.length;
for (let i = 0; i < n; i++) {
p1Lat = latArray[i] - latitude;
p1Long = longArray[i] - longitude;
p2Lat = latArray[(i+1)%n] - latitude;
p2Long = longArray[(i+1)%n] - longitude;
angle += angle2D(p1Lat,p1Long,p2Lat,p2Long);
}
return !(Math.abs(angle) < PI);
}
function angle2D(y1, x1, y2, x2)
{
let dtheta,theta1,theta2;
theta1 = Math.atan2(y1,x1);
theta2 = Math.atan2(y2,x2);
dtheta = theta2 - theta1;
while (dtheta > PI)
dtheta -= TWOPI;
while (dtheta < -PI)
dtheta += TWOPI;
return dtheta;
}
function isValidCoordinate(latitude,longitude)
{
return (
latitude !== '' && longitude !== '' && !isNaN(latitude)
&& !isNaN(longitude) && latitude > -90 &&
latitude < 90 && longitude > -180 && longitude < 180
)
}
let latArray = [32.10458, 32.10479, 32.1038, 32.10361];
let longArray = [34.86448, 34.86529, 34.86563, 34.86486];
// true
console.log(isCoordinateInsidePitch(32.104447, 34.865108,latArray, longArray));
// false
// isCoordinateInsidePitch(32.104974, 34.864576,latArray, longArray);
// true
// isValidCoordinate(0, 0)
// true
// isValidCoordinate(32.104974, 34.864576)
}
Assuming you handle the case of wrapping around the meridian and crossing the equator (by adding offsets) - can't you just treat this as a simple 2d point in polygon ?
Here is the algorithm written in Go:
It takes point coordinates in [lat,long] format and polygon in format [[lat,long],[lat,long]...]. Algorithm will join the first and last point in the polygon slice
import "math"
// ContainsLocation determines whether the point is inside the polygon
func ContainsLocation(point []float64, polygon [][]float64, geodesic
bool) bool {
size := len(polygon)
if size == 0 {
return false
}
var (
lat2, lng2, dLng3 float64
)
lat3 := toRadians(point[0])
lng3 := toRadians(point[1])
prev := polygon[size-1]
lat1 := toRadians(prev[0])
lng1 := toRadians(prev[1])
nIntersect := 0
for _, v := range polygon {
dLng3 = wrap(lng3-lng1, -math.Pi, math.Pi)
// Special case: point equal to vertex is inside.
if lat3 == lat1 && dLng3 == 0 {
return true
}
lat2 = toRadians(v[0])
lng2 = toRadians(v[1])
// Offset longitudes by -lng1.
if intersects(lat1, lat2, wrap(lng2-lng1, -math.Pi, math.Pi), lat3, dLng3, geodesic) {
nIntersect++
}
lat1 = lat2
lng1 = lng2
}
return (nIntersect & 1) != 0
}
func toRadians(p float64) float64 {
return p * (math.Pi / 180.0)
}
func wrap(n, min, max float64) float64 {
if n >= min && n < max {
return n
}
return mod(n-min, max-min) + min
}
func mod(x, m float64) float64 {
return math.Remainder(math.Remainder(x, m)+m, m)
}
func intersects(lat1, lat2, lng2, lat3, lng3 float64, geodesic bool) bool {
// Both ends on the same side of lng3.
if (lng3 >= 0 && lng3 >= lng2) || (lng3 < 0 && lng3 < lng2) {
return false
}
// Point is South Pole.
if lat3 <= -math.Pi/2 {
return false
}
// Any segment end is a pole.
if lat1 <= -math.Pi/2 || lat2 <= -math.Pi/2 || lat1 >= math.Pi/2 || lat2 >= math.Pi/2 {
return false
}
if lng2 <= -math.Pi {
return false
}
linearLat := (lat1*(lng2-lng3) + lat2*lng3) / lng2
// Northern hemisphere and point under lat-lng line.
if lat1 >= 0 && lat2 >= 0 && lat3 < linearLat {
return false
}
// Southern hemisphere and point above lat-lng line.
if lat1 <= 0 && lat2 <= 0 && lat3 >= linearLat {
return true
}
// North Pole.
if lat3 >= math.Pi/2 {
return true
}
// Compare lat3 with latitude on the GC/Rhumb segment corresponding to lng3.
// Compare through a strictly-increasing function (tan() or mercator()) as convenient.
if geodesic {
return math.Tan(lat3) >= tanLatGC(lat1, lat2, lng2, lng3)
}
return mercator(lat3) >= mercatorLatRhumb(lat1, lat2, lng2, lng3)
}
func tanLatGC(lat1, lat2, lng2, lng3 float64) float64 {
return (math.Tan(lat1)*math.Sin(lng2-lng3) + math.Tan(lat2)*math.Sin(lng3)) / math.Sin(lng2)
}
func mercator(lat float64) float64 {
return math.Log(math.Tan(lat*0.5 + math.Pi/4))
}
func mercatorLatRhumb(lat1, lat2, lng2, lng3 float64) float64 {
return (mercator(lat1)*(lng2-lng3) + mercator(lat2)*lng3) / lng2
}
Runner.Java code in VB.NET
For the benefit of .NET folks the same code is put in VB.NET. Have tried it and is quite fast. Tried with 350000 records, it finishes in just few minutes.
But as said by author, i'm yet to test scenarios intersecting equator, multizones etc.
'Usage
If coordinate_is_inside_polygon(CurLat, CurLong, Lat_Array, Long_Array) Then
MsgBox("Location " & CurLat & "," & CurLong & " is within polygon boundary")
Else
MsgBox("Location " & CurLat & "," & CurLong & " is NOT within polygon boundary")
End If
'Functions
Public Function coordinate_is_inside_polygon(ByVal latitude As Double, ByVal longitude As Double, ByVal lat_array() As Double, ByVal long_array() As Double) As Boolean
Dim i As Integer
Dim angle As Double = 0
Dim point1_lat As Double
Dim point1_long As Double
Dim point2_lat As Double
Dim point2_long As Double
Dim n As Integer = lat_array.Length()
For i = 0 To n - 1
point1_lat = lat_array(i) - latitude
point1_long = long_array(i) - longitude
point2_lat = lat_array((i + 1) Mod n) - latitude
point2_long = long_array((i + 1) Mod n) - longitude
angle += Angle2D(point1_lat, point1_long, point2_lat, point2_long)
Next
If Math.Abs(angle) < PI Then Return False Else Return True
End Function
Public Function Angle2D(ByVal y1 As Double, ByVal x1 As Double, ByVal y2 As Double, ByVal x2 As Double) As Double
Dim dtheta, theta1, theta2 As Double
theta1 = Math.Atan2(y1, x1)
theta2 = Math.Atan2(y2, x2)
dtheta = theta2 - theta1
While dtheta > PI
dtheta -= TWOPI
End While
While dtheta < -PI
dtheta += TWOPI
End While
Return (dtheta)
End Function
Public Function is_valid_gps_coordinate(ByVal latitude As Double, ByVal longitude As Double) As Boolean
If latitude > -90 AndAlso latitude < 90 AndAlso longitude > -180 AndAlso longitude < 180 Then
Return True
End If
Return False
End Function

Resources