How to check if a circle is "outside" of a polygon? - math

I'm trying to write a function to check if a circle is contained inside a polygon.
My algorithm is:
if they intersect, return false
if the circle is "outside" of the polygon, return false
return true
by outside, I mean something like this:
I thought about creating a line segment between the center of the circle and the center of the polygon, and if they intersect an edge on the polygon then the circle is outside of the polygon.
But is there another way to do it?

If the centre of the circle is inside the polygon, the circle is either overlapping or inside it.
If any point of the polygon lies on or inside the circle, they are overlapping.
If any line-segment of the polygon is intersecting the circle, they are overlapping.
Otherwise the circle is outside the polygon.
Something like this should work (C++):
#include <iostream>
#include <vector>
#include <cmath>
namespace math
{
struct Point
{
int x;
int y;
};
bool PointInCircle(Point p, Point center, std::size_t radius)
{
return ((p.x - center.x) * (p.x - center.x) + (p.y - center.y) * (p.y - center.y)) < (radius * radius);
}
bool PointOnCircle(Point p, Point center, std::size_t radius)
{
return ((p.x - center.x) * (p.x - center.x) + (p.y - center.y) * (p.y - center.y)) == (radius * radius);
}
bool LineIntersectsCircle(int ax, int by, int c, Point center, std::size_t radius)
{
// radius == distance = touching/tangent
// radius > distance = not intersecting
// radius < distance = intersecting
int distance = (std::abs(ax * center.x + by * center.y + c)) / sqrt(ax * ax + by * by);
return distance <= radius;
}
bool PointInPolygon(Point p, std::vector<Point> poly)
{
bool result = false;
std::size_t j = poly.size();
for (std::size_t i = 0; i < poly.size(); ++i)
{
if (((poly[i].y <= p.y && p.y < poly[j].y) || (poly[j].y <= p.y && p.y < poly[i].y))
&&
p.x < ((poly[j].x - poly[i].x) * (p.y - poly[i].y) / (poly[j].y - poly[i].y) + poly[i].x))
{
result = !result;
}
j = i;
}
return result;
}
bool CircleInsidePolygon(Point center, std::size_t radius, std::vector<Point> poly)
{
// Circle with radius 0 isn't a circle
if (radius == 0)
{
return false;
}
// If the center of the circle is not within the polygon,
// then the circle may overlap, but it'll never be "contained"
// so return false
if (!PointInPolygon(center, poly))
{
return false;
}
for (std::size_t i = 0; i < poly.size(); ++i)
{
// If any point of the polygon is within the circle,
// the circle is not "contained"
// so return false
if (PointInCircle(poly[i], center, radius))
{
return false;
}
}
for (std::size_t i = 0; i < poly.size(); ++i)
{
// If any line-segment of the polygon intersects the circle,
// the circle is not "contained"
// so return false
Point P1 = i == 0 ? poly[0] : poly[i];
Point P2 = i == 0 ? poly[poly.size() - 1] : poly[i + 1];
int X1 = P1.x;
int X2 = P2.x;
int Y1 = P1.y;
int Y2 = P2.y;
int A = Y1 - Y2;
int B = X2 - X1;
int C = (X1 * Y2) - (X2 * Y1);
if (LineIntersectsCircle(A, B, C, center, radius))
{
return false;
}
}
return true;
}
bool CircleOutsidePolygon(Point center, std::size_t radius, std::vector<Point> poly)
{
// Circle with radius 0 isn't a circle
if (radius == 0)
{
return false;
}
// If the center of the circle is within the polygon,
// the circle is not outside of the polygon completely.
// so return false.
if (PointInPolygon(center, poly))
{
return false;
}
for (std::size_t i = 0; i < poly.size(); ++i)
{
// If any point of the polygon is within the circle,
// or any point of the polygon lies on the circle,
// the circle is not outside of the polygon
// so return false.
if (PointInCircle(poly[i], center, radius) || PointOnCircle(poly[i], center, radius))
{
return false;
}
}
for (std::size_t i = 0; i < poly.size(); ++i)
{
// If any line-segment of the polygon intersects the circle,
// the circle is not outside the polygon, it is overlapping,
// so return false
Point P1 = i == 0 ? poly[0] : poly[i];
Point P2 = i == 0 ? poly[poly.size() - 1] : poly[i + 1];
int X1 = P1.x;
int X2 = P2.x;
int Y1 = P1.y;
int Y2 = P2.y;
int A = Y1 - Y2;
int B = X2 - X1;
int C = (X1 * Y2) - (X2 * Y1);
if (LineIntersectsCircle(A, B, C, center, radius))
{
return false;
}
}
return true;
}
}
int main()
{
std::size_t radius = 1;
math::Point p = {-2, -2};
std::vector<math::Point> poly = {
{0, 0},
{100, 0},
{100, 100},
{0, 100}
};
bool is_inside = math::CircleInsidePolygon(p, radius, poly);
bool is_outside = math::CircleOutsidePolygon(p, radius, poly);
bool is_intersecting = !is_inside && !is_outside;
std::cout<<"Circle In Polygon: "<<std::boolalpha<<is_inside<<"\n";
std::cout<<"Circle Outside Polygon: "<<std::boolalpha<<is_outside<<"\n";
std::cout<<"Circle Intersecting Polygon: "<<std::boolalpha<<is_intersecting<<"\n";
return 0;
}

You have a polygon defined by a list of points A0, A1, A2, ..., Ak, and a circle defined by a center C and a radius r.
Possible algorithm:
Find whether the center C is inside the polygon or not;
If C is outside the polygon, return False.
Else, find the point P on the polygon which is closest to the circle center C;
Compare the distance PC with the radius r.
Return the boolean value r < PC.
This leaves us with two sub-problems to solve:
How to find whether point C is inside the polygon;
How to find the closest point on the polygon.
Related questions solving these sub-problems:
Distance from a point to a polygon;
javascript: How can I find closest point on a polygon from a point?;
How can I determine whether a 2D Point is within a Polygon?;
javascript: Check if Point Is Inside A Polygon;
C: Point in polygon algorithm;
python matplotlib: What's the fastest way of checking if a point is inside a polygon in python.

Related

Calculating rotation of equally spaced items tangent to spiral

I'd like to programmatically draw a shape like this where there is an underlying spiral and equally spaced objects along it, placed tangent to the spiral as shown in this sketch:
I found an example of how to determine equally spaced points along the spiral here and am now trying to place hemispheres along the spiral. However, I'm not sure how to calculate the angle the shape needs to be rotated.
This is what I have so far (viewable here):
var totalSegments = 235,hw = 320,hh = 240,segments;
var len = 15;
points = [];
function setup(){
createCanvas(640,480);
smooth();
colorMode(HSB,255,100,100);
stroke(0);
noFill();
//println("move cursor vertically");
}
function draw(){
background(0);
translate(hw,hh);
segments = floor(totalSegments);
points = getTheodorus(segments,len);
angles = getAngles(segments, len);
for(var i = 0 ; i < segments ; i++){
let c = color('blue');
fill(c);
noStroke();
// draw shape
if(i % 2){
// console.log(i, ' ', angles[i]);
// try rotating around the object's center
push();
// translate(points[i].x, points[i].y)
rotate(PI/angles[i]);
arc(points[i].x, points[i].y, len*3, len*3, 0, 0 + PI);
pop();
}
// draw spiral
strokeWeight(20);
stroke(0,0,100,(20+i/segments));
if(i > 0) line(points[i].x,points[i].y,points[i-1].x,points[i-1].y);
}
}
function getAngles(segment, len){
let angles = [];
let radius = 0;
let angle = 0;
for(var i =0; i < segments; i++){
radius = sqrt(i+1);
angle += asin(1/radius);
angles[i] = angle;
}
return angles;
}
function getTheodorus(segments,len){
var result = [];
var radius = 0;
var angle = 0;
for(var i = 0 ; i < segments ; i++){
radius = sqrt(i+1);
angle += asin(1/radius);
result[i] = new p5.Vector(cos(angle) * radius*len,sin(angle) * radius*len);
}
return result;
}
Note that your drawing shows Archimedean spiral while link refers to Theodorus one.
Archimedean spiral is described by equation in polar coordinates (rho-theta)
r = a + b * Theta
where a is initial angle, b is scale value (describes distance between arms), r is radius.
And angle Theta + Pi/2 describes normal to spiral in point at parameter Theta
If you need an approximation to divide spiral into (almost) equal segments - use Clackson formula (example here)
theta = 2 * Pi * Sqrt(2 * s / b)
for arc length s

Calculate sound value with distance

I have a more mathematical than programming question, sorry if I'm not in the right section. In my 2D game, we can move the camera on a map where there are objects that can emit sound, and this sound volume (defined by a float from 0 to 1) must increase when the screen center is near this object. For example, when the object is at the screen center, the sound volume is 1, and when we move away, the volume must decrease. Each object has its own scope value. (for example 1000 pixels).
I don't know how to write a method that can calculate it.
Here is some of my code (which is not the right calculation) :
private function setVolumeWithDistance():Void
{
sound.volume = getDistanceFromScreenCenter() / range;
// So the volume is a 0 to 1 float, the range is the scope in pixels and
// and the getDistanceFromScreenCenter() is the distance in pixels
}
I already have the method which calculates the distance of the object from the center screen :
public function getDistanceFromScreenCenter():Float
{
return Math.sqrt(Math.pow((Cameraman.getInstance().getFocusPosition().x - position.x), 2) +
Math.pow((Cameraman.getInstance().getFocusPosition().y - position.y), 2));
Simple acoustics can help.
Here is the formula for sound intensity from a point source. It follows an inverse square of distance rule. Build that into your code.
You need to consider the mapping between global and screen coordinates. You have to map pixel location on the screen to physical coordinates and back.
Your distance code is flawed. No one should use pow() to square numbers. Yours is susceptible to round off errors.
This code combines the distance calculation, done properly, and attempts to solve the inverse square intensity calculation. Note: Inverse square is singular for zero distance.
package physics;
/**
* Simple model for an acoustic point source
* Created by Michael
* Creation date 1/16/2016.
* #link https://stackoverflow.com/questions/34827629/calculate-sound-value-with-distance/34828300?noredirect=1#comment57399595_34828300
*/
public class AcousticPointSource {
// Units matter here....
private static final double DEFAULT_REFERENCE_INTENSITY = 0.01;
private static final double DEFAULT_REFERENCE_DISTANCE = 1.0;
// Units matter here...
private double referenceDistance;
private double referenceIntensity;
public static void main(String[] args) {
int numPoints = 20;
double x = 0.0;
double dx = 0.05;
AcousticPointSource source = new AcousticPointSource();
for (int i = 0; i < numPoints; ++i) {
x += dx;
Point p = new Point(x);
System.out.println(String.format("point %s intensity %-10.6f", p, source.intensity(p)));
}
}
public AcousticPointSource() {
this(DEFAULT_REFERENCE_DISTANCE, DEFAULT_REFERENCE_INTENSITY);
}
public AcousticPointSource(double referenceDistance, double referenceIntensity) {
if (referenceDistance <= 0.0) throw new IllegalArgumentException("distance must be positive");
if (referenceIntensity <= 0.0) throw new IllegalArgumentException("intensity must be positive");
this.referenceDistance = referenceDistance;
this.referenceIntensity = referenceIntensity;
}
public double distance2D(Point p1) {
return distance2D(p1, Point.ZERO);
}
public double distance2D(Point p1, Point p2) {
double distance = 0.0;
if ((p1 != null) && (p2 != null)) {
double dx = Math.abs(p1.x - p2.x);
double dy = Math.abs(p1.y - p2.y);
double ratio;
if (dx > dy) {
ratio = dy/dx;
distance = dx;
} else {
ratio = dx/dy;
distance = dy;
}
distance *= Math.sqrt(1.0 + ratio*ratio);
if (Double.isNaN(distance)) {
distance = 0.0;
}
}
return distance;
}
public double intensity(Point p) {
double intensity = 0.0;
if (p != null) {
double distance = distance2D(p);
if (distance != 0.0) {
double ratio = this.referenceDistance/distance;
intensity = this.referenceIntensity*ratio*ratio;
}
}
return intensity;
}
}
class Point {
public static final Point ZERO = new Point(0.0, 0.0, 0.0);
public final double x;
public final double y;
public final double z;
public Point(double x) {
this(x, 0.0, 0.0);
}
public Point(double x, double y) {
this(x, y, 0.0);
}
public Point(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
#Override
public String toString() {
return String.format("(%-10.4f,%-10.4f,%-10.4f)", x, y, z);
}
}

Using Recursion for 3D Array Manipulation -- Causing StackOverflow (not Infinite!)

I recently posted a question yesterday about a similar issue, but I have coded up something a little different and now have a different problem. Here is my code that is causing a StackOverflow.
** Note that the 3D grid array is upwards of 1 million elements and can reach up to around 64 million elements (stores enums).
** Also note that this is not going into infinity. On small data sets, this algorithm works fine.
Is this likely caused by the extreme recursion? How do I handle this (this is an essential part of my algorithm!)? I've done some research and have heard using a queue, for even just massive for-loops.
What will reduce the likelihood of causing a stackoverflow?
Thank you!
/**
* Fills all void cells in the 3D grid of Atom.
*
* #param x
* The starting x coordinate
* #param y
* The starting y coordinate
* #param z
* The starting z coordinate
*/
private void fillAllVoidCells(int x, int y, int z)
{
// Base case -- If not BLOATED_ATOM, BOUNDING_BOX,
// or VOID then must be a cavity (only 4 CellType
// enum types.
if ((grid[x][y][z] == CellType.BLOATED_ATOM)
|| grid[x][y][z] == CellType.BOUNDING_BOX
|| grid[x][y][z] == CellType.VOID)
{
// Pop off runtime stack
return;
}
else
{
// Set to void then check all surrounding cells.
grid[x][y][z] = CellType.VOID;
fillAllVoidCells(x + 1, y, z); // right
fillAllVoidCells(x - 1, y, z); // left
fillAllVoidCells(x, y + 1, z); // in front
fillAllVoidCells(x, y - 1, z); // behind
fillAllVoidCells(x, y, z + 1); // above
fillAllVoidCells(x, y, z - 1); // below
}
}
===== EDIT ====== New Method Implemented Using a Stack (per Roee Gavirel help)
Would this be a correct implementation?
// ----------------------------------------------------------
/**
* Fills all void cells in the 3D grid of Atom.
*
* #param x
* The starting x coordinate
* #param y
* The starting y coordinate
* #param z
* The starting z coordinate
*/
private void fillAllVoidCells(int x, int y, int z)
{
Point p = new Point(x, y, z);
stack.push(p);
while (!stack.isEmpty())
p = stack.top();
stack.pop();
// Base case -- If not BLOATED_ATOM, BOUNDING_BOX,
// or VOID then must be a cavity (only 4 CellType
// enum types.
CellType state = grid[p.x][p.y][p.z];
if ((state == CellType.BLOATED_ATOM) || state == CellType.BOUNDING_BOX
|| state == CellType.VOID)
{
return;
}
else
{
// Set to void then check all surrounding cells.
grid[p.x][p.y][p.z] = CellType.VOID;
Point tempP = p;
tempP.x = p.x - 1;
stack.push(tempP);
tempP.x = p.x + 1;
stack.push(tempP);
tempP.x = p.x; // return to original x coordinate
tempP.y = p.y - 1;
stack.push(tempP);
tempP.y = p.y + 1;
stack.push(tempP);
tempP.y = p.y; // return to original y coordiante
tempP.z = p.z - 1;
stack.push(tempP);
tempP.z = p.z + 1;
stack.push(tempP);
tempP.z = p.z; // return to original z coordinate
}
}
This is most likely to cause an overflow. what you can (and should) do to avoid it is to use your own stack for the data and avoid recursion.
In you case:
1. have a stack of relevant points (x,y,z) which have the point you initially called fillAllVoidCells with.
2. while the stack is not empty you should do your checks
3. If it's cavity add the surrounding points to the stack.
==EDIT==
something like that:
struct point {
int x,y,z;
}
private void fillAllVoidCells(int x, int y, int z)
{
std::list<point> Ps;
point p;
p.x = x;
p.y = y;
p.z = z;
Ps.push_back(p);
while (!Ps.empty())
p = Ps.back();
Ps.pop_back();
// Base case -- If not BLOATED_ATOM, BOUNDING_BOX,
// or VOID then must be a cavity (only 4 CellType
// enum types.
auto state = grid[p.x][p.y][p.z];
if ((state == CellType.BLOATED_ATOM)
|| state == CellType.BOUNDING_BOX
|| state == CellType.VOID)
{
continue;
}
else
{
// Set to void then check all surrounding cells.
grid[p.x][p.y][p.z] = CellType.VOID;
point tempP = p;
tempP.x = P.x - 1;
Ps.push_back(tempP);
tempP.x = P.x + 1;
Ps.push_back(tempP);
tempP.y = P.y - 1;
Ps.push_back(tempP);
tempP.y = P.y + 1;
Ps.push_back(tempP);
tempP.z = P.z - 1;
Ps.push_back(tempP);
tempP.z = P.z + 1;
Ps.push_back(tempP);
}
}
}

Android - trouble in implementing this user interface

I am trying to implement a UI like this one..
http://www.shrenikvikam.com/wp-content/uploads/2011/04/214e422a43E11S3.png-150x134.png
But i am having some trouble implementing this.. Could someone tell me mistakes in this...
public class Meter extends View{
static final int ORBIT_COLOR = Color.argb(255, 66, 66, 66);
static final double RAD_CIRCLE = 2*Math.PI; // Number radians in a circle
private Paint paint; // Paint object controlling format of screen draws
private ShapeDrawable planet; // Planet symbol
private int planetRadius = 7; // Radius of spherical planet (pixels)
private int sunRadius = 12; // Radius of Sun (pixels)
private float X0 = 0; // X offset from center (pixels)
private float Y0 = 0; // Y offset from center (pixels)
private float X; // Current X position of planet (pixels)
private float Y; // Current Y position of planet (pixels)
private float centerX; // X for center of display (pixels)
private float centerY; // Y for center of display (pixels)
private float R0; // Radius of circular orbit (pixels)
private int nsteps = 120; // Number animation steps around circle
private double theta; // Angle around orbit (radians)
private double dTheta; // Angular increment each step (radians)
private double direction = -1; // Direction: counter-clockwise -1; clockwise +1
private float lastTouchX; // x coordinate of symbol i at last touch
private float lastTouchY; // x coordinate of symbol i at last touch
private int divisions = 120; // Since it requires temperature change from 0 -120
private double oneSegmentLength = (2 * Math.PI * R0)/(double)120;
public Meter(Context context) {
super(context);
// Initialize angle and angle step (in radians)
theta = 30;
//dTheta = RAD_CIRCLE/((double) nsteps); // Angle increment in radians
dTheta = ((360-60)/(double)divisions);
planet = new ShapeDrawable(new OvalShape());
planet.getPaint().setColor(Color.WHITE);
planet.setBounds(0, 0, 2*planetRadius, 2*planetRadius);
paint = new Paint();
paint.setAntiAlias(true);
paint.setTextSize(14);
paint.setStrokeWidth(1);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
// MotionEvent class constant signifying a finger-down event
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
lastTouchX = x;
lastTouchY = y;
newXY();
break;
}
// MotionEvent class constant signifying a finger-drag event
case MotionEvent.ACTION_MOVE: {
float newX = ev.getX();
float newY = ev.getY();
float dx = newX - lastTouchX;
float dy = newY - lastTouchY;
int diff = (int) (Math.abs(ev.getX()) % Math.abs(oneSegmentLength));
if(diff == 0){
if(Math.abs(dx) > Math.abs(dy)) {
if(dx>0) direction = 1;
else direction = -1;
newXY();
} else {
newXY();
}
Log.d("MOVE", "dx ->" + dx + " one seg->" + oneSegmentLength);
invalidate();
}
break;
}
// MotionEvent class constant signifying a finger-up event
case MotionEvent.ACTION_UP: {
Log.d("ACTION MOVE","Value ->");
final float x = ev.getX();
final float y = ev.getY();
lastTouchX = x;
lastTouchY = y;
newXY();
break;
}
}
return true;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawBackground(paint, canvas);
canvas.save();
canvas.translate(X + X0, Y + Y0);
planet.draw(canvas);
canvas.restore();
}
// Called by onDraw to draw the background
private void drawBackground(Paint paint, Canvas canvas){
paint.setColor(Color.YELLOW);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(centerX + X0, centerY + Y0, sunRadius, paint);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(ORBIT_COLOR);
canvas.drawCircle(centerX + X0, centerY + Y0, R0, paint);
}
//Method to increment angle theta and compute the new X and Y .
private void newXY(){
theta += dTheta;
Log.d("THETA VAL", "->" + theta);
//if(theta > RAD_CIRCLE) theta -= RAD_CIRCLE; // For convenience, keep angle 0-2pi
if(theta > 150)theta = 30;
if(theta > 30 && theta <120){
X = (float)(R0*Math.sin(direction*theta)) + centerX - planetRadius;
Y = centerY - (float)(R0*Math.cos(direction*theta)) - planetRadius;
}
//Log.i("ANIMATOR", "X="+X+" Y="+Y);
}
#Override
protected void onSizeChanged (int w, int h, int oldw, int oldh){
// Coordinates for center of screen
centerX = w/2;
centerY = h/2;
// Make orbital radius a fraction of minimum of width and height of display
R0 = (float) (0.90*Math.min(centerX, centerY));
oneSegmentLength = (2 * Math.PI * R0)/(double)120;
// Set the initial position of the planet (translate by planetRadius so center of planet
// is at this position)
X = centerX - planetRadius ;
Y = centerY - R0 - planetRadius;
}
}
I am referring this code to do this implementation...
http://eagle.phys.utk.edu/guidry/android/animatorDemo.html
I am just drawing a circle and trying to implement the same motion between 0 -120 degrees..

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