Skip to content

Commit e536a91

Browse files
committed
point in polygon routine, with tests
1 parent d6f983e commit e536a91

File tree

2 files changed

+262
-0
lines changed

2 files changed

+262
-0
lines changed

src/lib/polygon.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Copyright 2012-2015, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
/**
13+
* Turn an array of [x, y] pairs into a polygon object
14+
* that can test if points are inside it
15+
*
16+
* @param pts Array of [x, y] pairs
17+
*
18+
* @returns polygon Object {xmin, xmax, ymin, ymax, pts, contains}
19+
* (x|y)(min|max) are the bounding rect of the polygon
20+
* pts is the original array, with the first pair repeated at the end
21+
* contains is a function: (pt, omitFirstEdge)
22+
* pt is the [x, y] pair to test
23+
* omitFirstEdge truthy means points exactly on the first edge don't
24+
* count. This is for use adding one polygon to another so we
25+
* don't double-count the edge where they meet.
26+
* returns boolean: is pt inside the polygon (including on its edges)
27+
*/
28+
module.exports = function polygon(ptsIn) {
29+
var pts = ptsIn.slice(),
30+
xmin = pts[0][0],
31+
xmax = xmin,
32+
ymin = pts[0][1],
33+
ymax = ymin;
34+
35+
pts.push(pts[0]);
36+
for(var i = 1; i < pts.length; i++) {
37+
xmin = Math.min(xmin, pts[i][0]);
38+
xmax = Math.max(xmax, pts[i][0]);
39+
ymin = Math.min(ymin, pts[i][1]);
40+
ymax = Math.max(ymax, pts[i][1]);
41+
}
42+
43+
function contains(pt, omitFirstEdge) {
44+
var x = pt[0],
45+
y = pt[1];
46+
47+
if(x < xmin || x > xmax || y < ymin || y > ymax) {
48+
// pt is outside the bounding box of polygon
49+
return false;
50+
}
51+
52+
var imax = pts.length,
53+
x1 = pts[0][0],
54+
y1 = pts[0][1],
55+
crossings = 0,
56+
i,
57+
x0,
58+
y0,
59+
xmini,
60+
ycross;
61+
62+
for(i = 1; i < imax; i++) {
63+
// find all crossings of a vertical line upward from pt with
64+
// polygon segments
65+
// crossings exactly at xmax don't count, unless the point is
66+
// exactly on the segment, then it counts as inside.
67+
x0 = x1;
68+
y0 = y1;
69+
x1 = pts[i][0];
70+
y1 = pts[i][1];
71+
xmini = Math.min(x0, x1);
72+
73+
// outside the bounding box of this segment, it's only a crossing
74+
// if it's below the box.
75+
if(x < xmini || x > Math.max(x0, x1) || y > Math.max(y0, y1)) {
76+
continue;
77+
}
78+
else if(y < Math.min(y0, y1)) {
79+
// don't count the left-most point of the segment as a crossing
80+
// because we don't want to double-count adjacent crossings
81+
// UNLESS the polygon turns past vertical at exactly this x
82+
// Note that this is repeated below, but we can't factor it out
83+
// because
84+
if(x !== xmini) crossings++;
85+
}
86+
// inside the bounding box, check the actual line intercept
87+
else {
88+
// vertical segment - we know already that the point is exactly
89+
// on the segment, so mark the crossing as exactly at the point.
90+
if(x1 === x0) ycross = y;
91+
// any other angle
92+
else ycross = y0 + (x - x0) * (y1 - y0) / (x1 - x0);
93+
94+
// exactly on the edge: counts as inside the polygon, unless it's the
95+
// first edge and we're omitting it.
96+
if(y === ycross) {
97+
if(i === 1 && omitFirstEdge) return false;
98+
return true;
99+
}
100+
101+
if(y <= ycross && x !== xmini) crossings++;
102+
}
103+
}
104+
105+
// if we've gotten this far, odd crossings means inside, even is outside
106+
return crossings % 2 === 1;
107+
}
108+
109+
return {
110+
xmin: xmin,
111+
xmax: xmax,
112+
ymin: ymin,
113+
ymax: ymax,
114+
pts: pts,
115+
contains: contains
116+
};
117+
};

test/jasmine/tests/polygon_test.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
var polygon = require('@src/lib/polygon');
2+
3+
describe('polygon', function() {
4+
'use strict';
5+
6+
var squareCW = [[0, 0], [0, 1], [1, 1], [1, 0]],
7+
squareCCW = [[0, 0], [1, 0], [1, 1], [0, 1]],
8+
bowtie = [[0, 0], [0, 1], [1, 0], [1, 1]],
9+
squareish = [
10+
[-0.123, -0.0456],
11+
[0.12345, 1.2345],
12+
[1.3456, 1.4567],
13+
[1.5678, 0.21345]],
14+
equilateralTriangle = [
15+
[0, Math.sqrt(3) / 3],
16+
[-0.5, -Math.sqrt(3) / 6],
17+
[0.5, -Math.sqrt(3) / 6]],
18+
19+
zigzag = [ // 4 *
20+
[0, 0], [2, 1], // \-.
21+
[0, 1], [2, 2], // 3 * *
22+
[1, 2], [3, 3], // ,-' |
23+
[2, 4], [4, 3], // 2 *-* |
24+
[4, 0]], // ,-' |
25+
// 1 *---* |
26+
// ,-' |
27+
// 0 *-------*
28+
// 0 1 2 3 4
29+
inZigzag = [
30+
[0.5, 0.01], [1, 0.49], [1.5, 0.5], [2, 0.5], [2.5, 0.5], [3, 0.5],
31+
[3.5, 0.5], [0.5, 1.01], [1, 1.49], [1.5, 1.5], [2, 1.5], [2.5, 1.5],
32+
[3, 1.5], [3.5, 1.5], [1.5, 2.01], [2, 2.49], [2.5, 2.5], [3, 2.5],
33+
[3.5, 2.5], [2.5, 3.51], [3, 3.49]],
34+
notInZigzag = [
35+
[0, -0.01], [0, 0.01], [0, 0.99], [0, 1.01], [0.5, -0.01], [0.5, 0.26],
36+
[0.5, 0.99], [0.5, 1.26], [1, -0.01], [1, 0.51], [1, 0.99], [1, 1.51],
37+
[1, 1.99], [1, 2.01], [2, -0.01], [2, 2.51], [2, 3.99], [2, 4.01],
38+
[3, -0.01], [2.99, 3], [3, 3.51], [4, -0.01], [4, 3.01]],
39+
40+
donut = [ // inner CCW, outer CW // 3 *-----*
41+
[3, 0], [0, 0], [0, 1], [2, 1], [2, 2], // | |
42+
[1, 2], [1, 1], [0, 1], [0, 3], [3, 3]], // 2 | *-* |
43+
donut2 = [ // inner CCW, outer CCW // | | | |
44+
[3, 3], [0, 3], [0, 1], [2, 1], [2, 2], // 1 *-*-* |
45+
[1, 2], [1, 1], [0, 1], [0, 0], [3, 0]], // | |
46+
// 0 *-----*
47+
// 0 1 2 3
48+
inDonut = [[0.5, 0.5], [1, 0.5], [1.5, 0.5], [2, 0.5], [2.5, 0.5],
49+
[2.5, 1], [2.5, 1.5], [2.5, 2], [2.5, 2.5], [2, 2.5], [1.5, 2.5],
50+
[1, 2.5], [0.5, 2.5], [0.5, 2], [0.5, 1.5], [0.5, 1]],
51+
notInDonut = [[1.5, -0.5], [1.5, 1.5], [1.5, 3.5], [-0.5, 1.5], [3.5, 1.5]];
52+
53+
it('should exclude points outside the bounding box', function() {
54+
var poly = polygon([[1,2], [3,4]]);
55+
var pts = [[0, 3], [4, 3], [2, 1], [2, 5]];
56+
pts.forEach(function(pt) {
57+
expect(poly.contains(pt)).toBe(false);
58+
expect(poly.contains(pt, true)).toBe(false);
59+
expect(poly.contains(pt, false)).toBe(false);
60+
});
61+
});
62+
63+
it('should prepare a polygon object correctly', function() {
64+
var polyPts = [
65+
[[0, 0], [0, 1], [1, 1], [1, 0]],
66+
[[-2.34, -0.67], [0.12345, 1.2345], [1.3456, 1.4567], [1.5678, 0.21345]]
67+
];
68+
69+
polyPts.forEach(function(polyPt) {
70+
var poly = polygon(polyPt),
71+
xArray = polyPt.map(function(pt) { return pt[0]; }),
72+
yArray = polyPt.map(function(pt) { return pt[1]; });
73+
74+
expect(poly.pts.length).toEqual(polyPt.length + 1);
75+
polyPt.forEach(function(pt, i) {
76+
expect(poly.pts[i]).toEqual(pt);
77+
});
78+
expect(poly.pts[poly.pts.length - 1]).toEqual(polyPt[0]);
79+
expect(poly.xmin).toEqual(Math.min.apply(null, xArray));
80+
expect(poly.xmax).toEqual(Math.max.apply(null, xArray));
81+
expect(poly.ymin).toEqual(Math.min.apply(null, yArray));
82+
expect(poly.ymax).toEqual(Math.max.apply(null, yArray));
83+
});
84+
});
85+
86+
it('should include the whole boundary, except as per omitFirstEdge', function() {
87+
var polyPts = [squareCW, squareCCW, bowtie, squareish, equilateralTriangle,
88+
zigzag, donut, donut2];
89+
var np = 6; // number of intermediate points on each edge to test
90+
91+
polyPts.forEach(function(polyPt) {
92+
var poly = polygon(polyPt);
93+
poly.pts.forEach(function(pt1, i) {
94+
if(!i) return;
95+
var pt0 = poly.pts[i - 1],
96+
j;
97+
98+
var testPts = [pt0, pt1];
99+
for(j = 1; j < np; j++) {
100+
if(pt0[0] === pt1[0]) {
101+
testPts.push([pt0[0], pt0[1] + (pt1[1] - pt0[1]) * j / np]);
102+
}
103+
else {
104+
var x = pt0[0] + (pt1[0] - pt0[0]) * j / np;
105+
// calculated the same way as in the pt_in_polygon source,
106+
// so we know rounding errors will apply the same and this pt
107+
// *really* appears on the boundary
108+
testPts.push([x, pt0[1] + (x - pt0[0]) * (pt1[1] - pt0[1]) /
109+
(pt1[0] - pt0[0])]);
110+
}
111+
}
112+
testPts.forEach(function(pt, j) {
113+
expect(poly.contains(pt))
114+
.toBe(true, 'poly: ' + polyPt.join(';') + ', pt: ' + pt);
115+
var isFirstEdge = (i === 1) || (i === 2 && j === 0) ||
116+
(i === poly.pts.length - 1 && j === 1);
117+
expect(poly.contains(pt, true))
118+
.toBe(!isFirstEdge, 'omit: ' + !isFirstEdge + ', poly: ' +
119+
polyPt.join(';') + ', pt: ' + pt);
120+
});
121+
});
122+
});
123+
});
124+
125+
it('should find only the right interior points', function() {
126+
var zzpoly = polygon(zigzag);
127+
inZigzag.forEach(function(pt) {
128+
expect(zzpoly.contains(pt)).toBe(true);
129+
});
130+
notInZigzag.forEach(function(pt) {
131+
expect(zzpoly.contains(pt)).toBe(false);
132+
});
133+
134+
var donutpoly = polygon(donut),
135+
donut2poly = polygon(donut2);
136+
inDonut.forEach(function(pt) {
137+
expect(donutpoly.contains(pt)).toBe(true);
138+
expect(donut2poly.contains(pt)).toBe(true);
139+
});
140+
notInDonut.forEach(function(pt) {
141+
expect(donutpoly.contains(pt)).toBe(false);
142+
expect(donut2poly.contains(pt)).toBe(false);
143+
});
144+
});
145+
});

0 commit comments

Comments
 (0)