-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
apply patch from pelias/polygon-lookup#50 until a new release is cut
- Loading branch information
1 parent
aaa6e04
commit ff194d6
Showing
2 changed files
with
182 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
/** | ||
* Exports a `PolygonLookup` constructor, which constructs a data-structure for | ||
* quickly finding the polygon that a point intersects in a (potentially very | ||
* large) set. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
var Rbush = require( 'rbush' ); | ||
var pointInPolygon = require( 'point-in-polygon' ); | ||
var polygonUtils = require( './lib/polygon_utils' ); | ||
var _ = require('lodash'); | ||
|
||
/** | ||
* @property {rbush} rtree A spatial index for `this.polygons`. | ||
* @property {object} polgons A GeoJSON feature collection. | ||
* | ||
* @param {object} [featureCollection] An optional GeoJSON feature collection | ||
* to pass to `loadFeatureCollection()`. | ||
*/ | ||
function PolygonLookup( featureCollection ){ | ||
if( featureCollection !== undefined ){ | ||
this.loadFeatureCollection( featureCollection ); | ||
} | ||
} | ||
|
||
// Calculate point in polygon intersection, accounting for any holes | ||
function pointInPolygonWithHoles(point, polygons) { | ||
var mainPolygon = polygons.geometry.coordinates[0]; | ||
if( pointInPolygon( point, mainPolygon ) ){ | ||
for( var subPolyInd = 1; subPolyInd < polygons.geometry.coordinates.length; subPolyInd++ ){ | ||
if( pointInPolygon( point, polygons.geometry.coordinates[ subPolyInd ] ) ){ | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
|
||
/* | ||
* Internal helper method to return a single matching polygon | ||
*/ | ||
PolygonLookup.prototype.searchForOnePolygon = function searchForOnePolygon( x, y ) { | ||
// find which bboxes contain the search point. their polygons _may_ intersect that point | ||
var bboxes = this.rtree.search( { minX: x, minY: y, maxX: x, maxY: y } ); | ||
|
||
var point = [ x, y ]; | ||
|
||
// get the polygon for each possibly matching polygon based on the searched bboxes | ||
var polygons = bboxes.map(function(bbox, index) { | ||
return this.polygons[ bboxes[index].polyId ]; | ||
// find the first polygon that actually intersects and return it | ||
}, this); | ||
|
||
return _.find(polygons, function(polyObj) { | ||
return pointInPolygonWithHoles(point, polyObj); | ||
}); | ||
}; | ||
|
||
/* | ||
* Internal helper method to return multiple matching polygons, up to a given limit. | ||
* A limit of -1 means unlimited | ||
*/ | ||
PolygonLookup.prototype.searchForMultiplePolygons = function searchForMultiplePolygons( x, y, limit ) { | ||
if (limit === -1) { | ||
limit = Number.MAX_SAFE_INTEGER; | ||
} | ||
|
||
var point = [ x, y ]; | ||
var bboxes = this.rtree.search( { minX: x, minY: y, maxX: x, maxY: y } ); | ||
|
||
// get the polygon for each possibly matching polygon based on the searched bboxes | ||
var polygons = bboxes.map(function(bbox, index) { | ||
return this.polygons[ bboxes[index].polyId ]; | ||
}, this); | ||
|
||
// keep track of matches to avoid extra expensive calculations if limit reached | ||
var matchesFound = 0; | ||
|
||
// filter matching polygons, up to the limit | ||
polygons = polygons.filter(function(polygon) { | ||
// short circuit if limit reached | ||
if (matchesFound >= limit) { | ||
return false; | ||
} | ||
|
||
var intersects = pointInPolygonWithHoles(point, polygon); | ||
if (intersects) { | ||
matchesFound++; | ||
return true; | ||
} | ||
return false; | ||
}); | ||
|
||
// return all matching polygons as a GeoJSON FeatureCollection | ||
return { | ||
type : 'FeatureCollection', | ||
features : polygons, | ||
}; | ||
}; | ||
|
||
/** | ||
* Find polygon(s) that a point intersects. Execute a bounding-box search to | ||
* narrow down the candidate polygons to a small subset, and then perform | ||
* additional point-in-polygon intersections to resolve any ambiguities. | ||
* | ||
* @param {number} x The x-coordinate of the point. | ||
* @param {number} y The y-coordinate of the point. | ||
* @param {number} [limit] Number of results to return (-1 to return all the results). | ||
* @return {undefined|object} If one or more bounding box intersections are | ||
* found and limit is undefined, return the first polygon that intersects (`x`, `y`); otherwise, | ||
* `undefined`. If a limit is passed in, return intercecting polygons as a GeoJSON FeatureCollection. | ||
*/ | ||
PolygonLookup.prototype.search = function search( x, y, limit ){ | ||
if (limit === undefined) { | ||
return this.searchForOnePolygon( x, y ); | ||
} else { | ||
return this.searchForMultiplePolygons( x, y, limit ); | ||
} | ||
}; | ||
|
||
/** | ||
* Build a spatial index for a set of polygons, and store both the polygons and | ||
* the index in this `PolygonLookup`. | ||
* | ||
* @param {object} collection A GeoJSON-formatted FeatureCollection. | ||
*/ | ||
PolygonLookup.prototype.loadFeatureCollection = function loadFeatureCollection( collection ){ | ||
var bboxes = []; | ||
var polygons = []; | ||
var polyId = 0; | ||
|
||
function indexPolygon( poly ){ | ||
polygons.push(poly); | ||
var bbox = polygonUtils.getBoundingBox( poly.geometry.coordinates[ 0 ] ); | ||
bbox.polyId = polyId++; | ||
bboxes.push(bbox); | ||
} | ||
|
||
function indexFeature( poly ){ | ||
if( poly.geometry && | ||
poly.geometry.coordinates[ 0 ] !== undefined && | ||
poly.geometry.coordinates[ 0 ].length > 0){ | ||
switch( poly.geometry.type ){ | ||
case 'Polygon': | ||
indexPolygon( poly ); | ||
break; | ||
|
||
case 'MultiPolygon': | ||
var childPolys = poly.geometry.coordinates; | ||
for( var ind = 0; ind < childPolys.length; ind++ ){ | ||
var childPoly = { | ||
type: 'Feature', | ||
properties: poly.properties, | ||
geometry: { | ||
type: 'Polygon', | ||
coordinates: childPolys[ ind ] | ||
} | ||
}; | ||
if ('id' in poly) { | ||
childPoly.id = poly.id; | ||
} | ||
indexPolygon( childPoly ); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
|
||
collection.features.forEach( indexFeature ); | ||
this.rtree = new Rbush().load( bboxes ); | ||
this.polygons = polygons; | ||
}; | ||
|
||
module.exports = PolygonLookup; |