-
Notifications
You must be signed in to change notification settings - Fork 953
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial commit of boolean DE-9IM module #707
Changes from 5 commits
09abe34
93a3400
6a9b1ba
727c80a
ede82d6
d1644ec
ff3e6e3
e8c00d6
a3497f3
19ae32d
2a50cdc
65c2d0c
1839242
d40e629
c4bcb07
8bff7d6
6ecbc3e
d94a78e
fd442ae
f2b9531
87500ef
f4f8a18
cb32dd5
d772b9a
78a0af8
ba618ad
3d40ec8
7d715e6
c1c898c
300208b
2ab6a2b
0827683
f40b3d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
const Benchmark = require('benchmark'); | ||
const {point, multiPoint, lineString, polygon} = require('@turf/helpers'); | ||
const contains = require('./'); | ||
|
||
// Fixtures | ||
const pt = point([1, 1]); | ||
const mp = multiPoint([[1, 1], [12, 12]]); | ||
const line = lineString([[1, 1], [1, 2], [1, 3], [1, 4]]); | ||
const poly = polygon([[[1, 1], [1, 10], [10, 10], [10, 1], [1, 1]]]); | ||
|
||
/** | ||
* Benchmark Results | ||
* | ||
* point - point x 25,229,201 ops/sec ±2.10% (90 runs sampled) | ||
* point - multipoint: | ||
* point - line: | ||
* point - polygon: | ||
* multipoint - point x 865,610 ops/sec ±1.71% (86 runs sampled) | ||
* polygon - line x 991,549 ops/sec ±1.71% (90 runs sampled) | ||
*/ | ||
const suite = new Benchmark.Suite('turf-boolean-contains'); | ||
suite | ||
.add('point - point', () => contains(pt, pt)) | ||
.add('point - multipoint', () => contains(pt, mp)) | ||
.add('point - line', () => contains(pt, line)) | ||
.add('point - polygon', () => contains(pt, polygon)) | ||
.add('multipoint - point', () => contains(mp, pt)) | ||
.add('polygon - line', () => contains(poly, line)) | ||
.on('cycle', e => console.log(String(e.target))) | ||
.on('complete', () => {}) | ||
.run(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
var inside = require('@turf/inside'); | ||
var helpers = require('@turf/helpers'); | ||
var deepEqual = require('deep-equal'); | ||
var lineOverlap = require('@turf/line-overlap'); | ||
|
||
/** | ||
* Contains returns true if the second geometry is completely contained by the first geometry. | ||
* The contains predicate returns the exact opposite result of the within predicate. | ||
* | ||
* @name contains | ||
* @param {Geometry|Feature<any>} feature1 source | ||
* @param {Geometry|Feature<any>} feature2 target | ||
* @returns {Boolean} true/false | ||
* @example | ||
* var along = turf.contains(line, point); | ||
*/ | ||
module.exports = function (feature1, feature2) { | ||
var geom1 = getGeom(feature1); | ||
var geom2 = getGeom(feature2); | ||
|
||
switch (geom1.type) { | ||
case 'Point': | ||
switch (geom2.type) { | ||
case 'Point': | ||
return deepEqual(geom1.coordinates, geom2.coordinates); | ||
} | ||
throw new Error('feature2 ' + geom2.type + ' geometry not supported'); | ||
case 'MultiPoint': | ||
switch (geom2.type) { | ||
case 'Point': | ||
return isPointInMultiPoint(geom1, geom2); | ||
case 'MultiPoint': | ||
return isMultiPointInMultiPoint(geom1, geom2); | ||
} | ||
throw new Error('feature2 ' + geom2.type + ' geometry not supported'); | ||
case 'LineString': | ||
switch (geom2.type) { | ||
case 'Point': | ||
return isPointOnLine(geom1, geom2); | ||
case 'LineString': | ||
return isLineOnLine(geom1, geom2); | ||
case 'MultiPoint': | ||
return isMultiPointOnLine(geom1, geom2); | ||
} | ||
throw new Error('feature2 ' + geom2.type + ' geometry not supported'); | ||
case 'Polygon': | ||
switch (geom2.type) { | ||
case 'Point': | ||
return isPointInPoly(geom1, geom2); | ||
case 'LineString': | ||
return isLineInPoly(geom1, geom2); | ||
case 'Polygon': | ||
return isPolyInPoly(geom1, geom2); | ||
case 'MultiPoint': | ||
return isMultiPointInPoly(geom1, geom2); | ||
} | ||
throw new Error('feature2 ' + geom2.type + ' geometry not supported'); | ||
default: | ||
throw new Error('feature1 ' + geom1.type + ' geometry not supported'); | ||
} | ||
}; | ||
|
||
function isPointInMultiPoint(MultiPoint, Point) { | ||
var i; | ||
var output = false; | ||
for (i = 0; i < MultiPoint.coordinates.length; i++) { | ||
if (deepEqual(MultiPoint.coordinates[i], Point.coordinates)) { | ||
output = true; | ||
break; | ||
} | ||
} | ||
return output; | ||
} | ||
|
||
function isMultiPointInMultiPoint(MultiPoint1, MultiPoint2) { | ||
var foundAMatch = 0; | ||
for (var i = 0; i < MultiPoint2.coordinates.length; i++) { | ||
var anyMatch = false; | ||
for (var i2 = 0; i2 < MultiPoint1.coordinates.length; i2++) { | ||
if (deepEqual(MultiPoint2.coordinates[i], MultiPoint1.coordinates[i2])) { | ||
foundAMatch++; | ||
anyMatch = true; | ||
break; | ||
} | ||
} | ||
if (!anyMatch) { | ||
return false; | ||
} | ||
} | ||
return foundAMatch > 0 && foundAMatch < MultiPoint1.coordinates.length; | ||
} | ||
|
||
// http://stackoverflow.com/a/11908158/1979085 | ||
function isPointOnLine(LineString, Point) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be allowing the point to land on the line end points. That would place the point on the line boundary and therefore no point lies on the interior of the line (see above definition of contains). I think the end points should be excluded. |
||
var output = false; | ||
for (var i = 0; i < LineString.coordinates.length - 1; i++) { | ||
if (isPointOnLineSegment(LineString.coordinates[i], LineString.coordinates[i + 1], Point.coordinates)) { | ||
output = true; | ||
break; | ||
} | ||
} | ||
return output; | ||
} | ||
|
||
function isMultiPointOnLine(LineString, MultiPoint) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as above. No check that any point is in the interior of line. As you probably have noticed already the edge cases are the real pain when implementing these booleans functions. |
||
var output = true; | ||
for (var i = 0; i < MultiPoint.coordinates.length; i++) { | ||
var pointIsOnLine = false; | ||
for (var i2 = 0; i2 < LineString.coordinates.length - 1; i2++) { | ||
if (isPointOnLineSegment(LineString.coordinates[i2], LineString.coordinates[i2 + 1], MultiPoint.coordinates[i])) { | ||
pointIsOnLine = true; | ||
break; | ||
} | ||
} | ||
if (!pointIsOnLine) { | ||
output = false; | ||
break; | ||
} | ||
} | ||
return output; | ||
} | ||
|
||
function isPointInPoly(Polygon, Point) { | ||
return inside(Point, Polygon); | ||
} | ||
|
||
function isMultiPointInPoly(Polygon, MultiPoint) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same interior point issue as above. |
||
var output = true; | ||
for (var i = 0; i < MultiPoint.coordinates.length; i++) { | ||
var isInside = isPointInPoly(Polygon, helpers.point(MultiPoint.coordinates[1])); | ||
if (!isInside) { | ||
output = false; | ||
break; | ||
} | ||
} | ||
return output; | ||
} | ||
|
||
// TO DO - Work out how to check if line is in line (can potentially use line overlap module) | ||
// Also need to make sure lines are exactly the same, eg the second must be smaller than the first | ||
function isLineOnLine(LineString1, LineString2) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. t.equal(contains(line1, line3), false, 'A line fails that lies partially outside the other line'); Test fail because line should be completely on line (returns true because of partial match). Might be worth adding a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gday @DenisCarriere As a general comment I feel like the line booleans are the trickiest to get right at this stage, acknowledge that there is some work needed there There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Never said it was easy 👍 Keep it up! |
||
var output = true; | ||
var overlappingLine = lineOverlap(helpers.feature(LineString1), helpers.feature(LineString2)).features[0]; | ||
if (!overlappingLine) return false; | ||
overlappingLine = getGeom(overlappingLine); | ||
if (overlappingLine.coordinates[0] === LineString1.coordinates[0] && | ||
overlappingLine.coordinates[overlappingLine.coordinates.length - 1] === LineString1.coordinates[LineString1.coordinates.length - 1]) { | ||
output = false; | ||
} | ||
return output; | ||
} | ||
|
||
function isLineInPoly(Polygon, Linestring) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this approach will only work for convex polygons. Concave or polygons with holes can fail this. Think of a donut with a line that only crosses the hole. I think it would be safer to first check if the line intersects the polygon boundaries (including holes). If it doesn't then check that at least one point is in polygon interior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Polygons with holes will be hard to implement, @rowanwins I think at first lets just get it working with only outer rings and afterwards we work on the inner ring validation (donuts). |
||
var output = true; | ||
for (var i = 0; i < Linestring.coordinates.length; i++) { | ||
var isInside = isPointInPoly(Polygon, Linestring.coordinates[i]); | ||
if (!isInside) { | ||
output = false; | ||
break; | ||
} | ||
} | ||
return output; | ||
} | ||
|
||
// See http://stackoverflow.com/a/4833823/1979085 | ||
// TO DO - Need to handle if the polys are the same, eg there must be some outer coordinates different | ||
function isPolyInPoly(Polygon1, Polygon2) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this has the same issue as |
||
for (var i = 0; i < Polygon2.coordinates[0].length; i++) { | ||
if (!isPointInPoly(Polygon1, helpers.point(Polygon2.coordinates[0][i]))) { | ||
return false; | ||
} | ||
} | ||
|
||
if (deepEqual(Polygon1.coordinates, Polygon2.coordinates)) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
function isPointOnLineSegment(LineSegmentStart, LineSegmentEnd, Point) { | ||
var dxc = Point[0] - LineSegmentStart[0]; | ||
var dyc = Point[1] - LineSegmentStart[1]; | ||
var dxl = LineSegmentEnd[0] - LineSegmentStart[0]; | ||
var dyl = LineSegmentEnd[1] - LineSegmentStart[1]; | ||
var cross = dxc * dyl - dyc * dxl; | ||
if (cross !== 0) { | ||
return false; | ||
} | ||
if (Math.abs(dxl) >= Math.abs(dyl)) { | ||
return dxl > 0 ? LineSegmentStart[0] <= Point[0] && Point[0] <= LineSegmentEnd[0] : LineSegmentEnd[0] <= Point[0] && Point[0] <= LineSegmentStart[0]; | ||
} else { | ||
return dyl > 0 ? LineSegmentStart[1] <= Point[1] && Point[1] <= LineSegmentEnd[1] : LineSegmentEnd[1] <= Point[1] && Point[1] <= LineSegmentStart[1]; | ||
} | ||
} | ||
|
||
/** | ||
* Get Geometry from Feature or Geometry Object | ||
* | ||
* @private | ||
* @param {Feature<any>|Geometry<any>} geojson GeoJSON Feature or Geometry Object | ||
* @returns {Geometry<any>} GeoJSON Geometry Object | ||
* @throws {Error} if geojson is not a Feature or Geometry Object | ||
*/ | ||
function getGeom(geojson) { | ||
if (geojson.geometry) return geojson.geometry; | ||
if (geojson.coordinates) return geojson; | ||
throw new Error('geojson must be a feature or geometry object'); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
{ | ||
"name": "@turf/boolean-contains", | ||
"version": "4.0.0", | ||
"description": "turf boolean-contains module", | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"files": [ | ||
"index.js", | ||
"index.d.ts" | ||
], | ||
"scripts": { | ||
"test": "node test.js", | ||
"bench": "node bench.js" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/Turfjs/turf.git" | ||
}, | ||
"keywords": [ | ||
"turf", | ||
"contains", | ||
"boolean", | ||
"de-9im" | ||
], | ||
"author": "Turf Authors", | ||
"contributors": [ | ||
"Rowan Winsemius <@rowanwins>", | ||
"Denis Carriere <@DenisCarriere>" | ||
], | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "/~https://github.com/Turfjs/turf/issues" | ||
}, | ||
"homepage": "/~https://github.com/Turfjs/turf", | ||
"devDependencies": { | ||
"benchmark": "^2.1.4", | ||
"tape": "^4.6.3" | ||
}, | ||
"dependencies": { | ||
"@turf/helpers": "^4.1.0", | ||
"@turf/inside": "^4.1.0", | ||
"@turf/line-overlap": "^4.1.0", | ||
"deep-equal": "^1.0.1" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
const test = require('tape'); | ||
const {point, multiPoint, lineString, polygon} = require('@turf/helpers'); | ||
const contains = require('./'); | ||
|
||
test('turf-boolean-contains', t => { | ||
const pt1 = point([1, 1]); | ||
const pt2 = point([1, 4]); | ||
const pt3 = point([4, 4]); | ||
const pt4 = point([14, 14]); | ||
|
||
const mp1 = multiPoint([[1, 1], [12, 12]]); | ||
const mp2 = multiPoint([[1, 1], [1, 1.5]]); | ||
const mp3 = multiPoint([[1, 1], [12, 12], [15, 15]]); | ||
|
||
t.equal(contains(mp1, pt1), true, 'Point is contained within multipoint'); | ||
t.equal(contains(mp1, pt2), false, 'Point is not contained outside multipoint'); | ||
|
||
t.equal(contains(mp3, mp1), true, 'True if all multipoints are contained with within multipoints'); | ||
t.equal(contains(mp3, mp2), false, 'False if some multipoints are elsewhere'); | ||
|
||
const line1 = lineString([[1, 1], [1, 2], [1, 3], [1, 4]]); | ||
t.equal(contains(line1, pt1), true, 'Point is on line'); | ||
t.equal(contains(line1, pt3), false, 'Point is not on line'); | ||
|
||
t.equal(contains(line1, mp2), true, 'MultiPoint is on line'); | ||
t.equal(contains(line1, mp3), false, 'MultiPoint is not on line'); | ||
|
||
const poly1 = polygon([[[1, 1], [1, 10], [10, 10], [10, 1], [1, 1]]]); | ||
t.equal(contains(poly1, pt3), true, 'A point lies inside the polygon boundary'); | ||
t.equal(contains(poly1, pt1), true, 'A point lies on the polygon boundary'); | ||
t.equal(contains(poly1, pt4), false, 'A point lies outside the polygon boundary fails'); | ||
|
||
const mp4 = multiPoint([[1, 1], [4, 4]]); | ||
t.equal(contains(poly1, mp4), true, 'A multipoint lies inside the polygon boundary'); | ||
t.equal(contains(poly1, mp1), false, 'A multipoint with a point outside fails'); | ||
|
||
|
||
const line2 = lineString([[1, 2], [1, 3], [1, 3.5]]); | ||
const line3 = lineString([[1, 2], [1, 3], [1, 15.5]]); | ||
|
||
t.equal(contains(line1, line2), true, 'A line lies inside the other line'); | ||
t.equal(contains(line1, line3), false, 'A line fails that lies partially outside the other line'); | ||
|
||
t.equal(contains(poly1, line1), true, 'A line within the poly passes as true'); | ||
t.equal(contains(poly1, line3), false, 'A line that lies partially outside the poly is false'); | ||
|
||
|
||
const poly2 = polygon([[[1, 1], [1, 2], [1, 3], [1, 4], [1, 1]]]); | ||
const poly3 = polygon([[[1, 1], [1, 20], [1, 3], [1, 4], [1, 1]]]); | ||
|
||
t.equal(contains(poly1, poly2), true, 'A poly passes that is inside although allows touching edges'); | ||
t.equal(contains(poly1, poly3), false, 'A poly fails that has a vertice outside the poly'); | ||
|
||
t.end(); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
var internalHelpers = require('./helpers'); | ||
var checkIfDeepCoordArraysMatch = internalHelpers.checkIfDeepCoordArraysMatch; | ||
|
||
/** | ||
* Contains returns true if the second geometry is completely contained by the first geometry. | ||
* The contains predicate returns the exact opposite result of the within predicate. | ||
* | ||
* @name contains | ||
* @param {feature1} feature1 | ||
* @param {feature2} feature2 | ||
* @returns {Boolean} | ||
* @example | ||
* var along = turf.contains(line, point); | ||
*/ | ||
|
||
module.exports = function (feature1, feature2) { | ||
return checkIfDeepCoordArraysMatch(feature1.geometry.coordinates, feature2.geometry.coordinates) && feature1.geometry.type === feature2.geometry.type; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might want to add a warning that this is only testing that the coordinates are identical. It is not testing that the geometries are equal. For example, the lines There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one sounds like it would be real tricky to write. Would be worth including as a test case or add it to the JSDocs description. |
||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second condition here excludes the two geometries being equal. Not sure if that is actually in the definition of
contains
. According to DE-9IM the definition is:MultiPoint2
must be in the exterior ofMultiPoint1
MultiPoint2
must be in the interior ofMultiPoint1
Since the points themselves are their own interior, the two being equal would seem to meet that criteria. Could be wrong, the DE-9IM definitions are sometimes tricky to parse.