Skip to content

Commit

Permalink
#4 #7 experimental GeoJSON
Browse files Browse the repository at this point in the history
  • Loading branch information
Sean committed Jul 22, 2017
1 parent 59b391a commit 6e20104
Show file tree
Hide file tree
Showing 7 changed files with 517 additions and 7 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Boolean operations on polygons (union, intersection, difference, xor)
etc)
4. Uses formulas that take floating point irregularities into account (via configurable epsilon)
5. Provides an API for constructing efficient sequences of operations
6. Support for GeoJSON Polygon and MultiPolygon (experimental)

# Resources

Expand Down Expand Up @@ -79,6 +80,25 @@ Where `poly1`, `poly2`, and the return value are Polygon objects, in the format
}
```

# GeoJSON (experimental)

There are also functions for converting between the native polygon format and
[GeoJSON](https://tools.ietf.org/html/rfc7946).

Note: These functions are currently **experimental**, and I'm hoping users can provide feedback.
Please comment in [this issue on GitHub](https://github.com/voidqk/polybooljs/issues/7) -- including
letting me know if it's working as expected. I don't use GeoJSON, but I thought I would take a
crack at conversion functions.

Use the following functions:

```
var geojson = PolyBool.polygonToGeoJSON(poly);
var poly = PolyBool.polygonFromGeoJSON(geojson);
```

Only `"Polygon"` and `"MultiPolygon"` types are supported.

# Core API

```javascript
Expand Down
50 changes: 50 additions & 0 deletions dist/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,52 @@
build_log: BL
};
redraw();

// output GeoJSON

var geojson = PolyBool.polygonToGeoJSON(clipResult.result);
function scalePoly(p){
// we need to scale the result because pixel coordinates are around 500, and that's not
// valid long/lat coordinates... so we just divide everything by 10
// (and out of pure luck this tends to place our polygons over Ethiopia...!)
for (var i = 0; i < p.length; i++){
for (var j = 0; j < p[i].length; j++)
p[i][j] = [p[i][j][0] * 0.1, p[i][j][1] * 0.1];
}
}
// I suppose we could just JSON.stringify(geojson, null, ' '), but that doesn't look so
// pretty (imho), so this is a bit stupid but I format it myself so it looks better :-P
var out = ['{', ' "type": ' + JSON.stringify(geojson.type) + ','];
function outLine(line, space, tail){
var o = space + '[';
for (var i = 0; i < line.length; i++){
o += '[' + line[i] + ']';
if (i < line.length - 1)
o += ', ';
}
out.push(o + ']' + (tail ? '' : ','));
}
if (geojson.type == 'Polygon'){
scalePoly(geojson.coordinates);
out.push(' "coordinates": [');
for (var i = 0; i < geojson.coordinates.length; i++)
outLine(geojson.coordinates[i], ' ', i === geojson.coordinates.length - 1);
out.push(' ]');
}
else{
for (var i = 0; i < geojson.coordinates.length; i++)
scalePoly(geojson.coordinates[i]);
out.push(' "coordinates": [[');
for (var i = 0; i < geojson.coordinates.length; i++){
for (var j = 0; j < geojson.coordinates[i].length; j++)
outLine(geojson.coordinates[i][j], ' ', j === geojson.coordinates[i].length - 1);
if (i < geojson.coordinates.length - 1)
out.push(' ], [');
}
out.push(' ]]');
}
out.push('}', '');
document.getElementById('geojson').value = out.join('\n');
}

function scaleX(x){
Expand Down Expand Up @@ -1294,6 +1340,10 @@
<button onclick="javascript: nextDemo(-1);">Prev</button>
<button onclick="javascript: nextDemo(1);">Next</button>
</p>
<p>
<a href="https://tools.ietf.org/html/rfc7946">GeoJSON</a> of result (experimental):
</p>
<center><textarea id="geojson" rows="8" style="width: 650px;"></textarea></center>
<p>
Polygon Clipping Demo based somewhat on the F. Martinez et al. algorithm (2008)
</p>
Expand Down
231 changes: 226 additions & 5 deletions dist/polybool.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ var Epsilon = require('./lib/epsilon');
var Intersecter = require('./lib/intersecter');
var SegmentChainer = require('./lib/segment-chainer');
var SegmentSelector = require('./lib/segment-selector');
var GeoJSON = require('./lib/geojson');

var buildLog = false;
var epsilon = Epsilon();

var PolyBool = {
var PolyBool;
PolyBool = {
// getter/setter for buildLog
buildLog: function(bl){
if (bl === true)
Expand Down Expand Up @@ -85,6 +87,14 @@ var PolyBool = {
};
},

// GeoJSON converters
polygonFromGeoJSON: function(geojson){
return GeoJSON.toPolygon(PolyBool, geojson);
},
polygonToGeoJSON: function(poly){
return GeoJSON.fromPolygon(PolyBool, epsilon, poly);
},

// helper functions for common operations
union: function(poly1, poly2){
return operate(poly1, poly2, PolyBool.selectUnion);
Expand Down Expand Up @@ -116,7 +126,7 @@ if (typeof window === 'object')

module.exports = PolyBool;

},{"./lib/build-log":2,"./lib/epsilon":3,"./lib/intersecter":4,"./lib/segment-chainer":6,"./lib/segment-selector":7}],2:[function(require,module,exports){
},{"./lib/build-log":2,"./lib/epsilon":3,"./lib/geojson":4,"./lib/intersecter":5,"./lib/segment-chainer":7,"./lib/segment-selector":8}],2:[function(require,module,exports){
// (c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc
// MIT License
// Project Home: https://github.com/voidqk/polybooljs
Expand Down Expand Up @@ -375,6 +385,27 @@ function Epsilon(eps){
ret.alongB = 2;

return ret;
},
pointInsideRegion: function(pt, region){
var x = pt[0];
var y = pt[1];
var last_x = region[region.length - 1][0];
var last_y = region[region.length - 1][1];
var inside = false;
for (var i = 0; i < region.length; i++){
var curr_x = region[i][0];
var curr_y = region[i][1];

// if y is between curr_y and last_y, and
// x is to the right of the boundary created by the line
if ((curr_y - y > eps) != (last_y - y > eps) &&
(last_x - curr_x) * (y - curr_y) / (last_y - curr_y) + curr_x - x > eps)
inside = !inside

last_x = curr_x;
last_y = curr_y;
}
return inside;
}
};
return my;
Expand All @@ -383,6 +414,196 @@ function Epsilon(eps){
module.exports = Epsilon;

},{}],4:[function(require,module,exports){
// (c) Copyright 2017, Sean Connelly (@voidqk), http://syntheti.cc
// MIT License
// Project Home: https://github.com/voidqk/polybooljs

//
// convert between PolyBool polygon format and GeoJSON formats (Polygon and MultiPolygon)
//

var GeoJSON = {
// convert a GeoJSON object to a PolyBool polygon
toPolygon: function(PolyBool, geojson){

// converts list of LineString's to segments
function GeoPoly(coords){
// check for empty coords
if (coords.length <= 0)
return PolyBool.segments({ inverted: false, regions: [] });

// convert LineString to segments
function LineString(ls){
// remove tail which should be the same as head
var reg = ls.slice(0, ls.length - 1);
return PolyBool.segments({ inverted: false, regions: [reg] });
}

// the first LineString is considered the outside
var out = LineString(coords[0]);

// the rest of the LineStrings are considered interior holes, so subtract them from the
// current result
for (var i = 1; i < coords.length; i++)
out = PolyBool.selectDifference(PolyBool.combine(out, LineString(coords[i])));

return out;
}

if (geojson.type === 'Polygon'){
// single polygon, so just convert it and we're done
return PolyBool.polygon(GeoPoly(geojson.coordinates));
}
else if (geojson.type === 'MultiPolygon'){
// multiple polygons, so union all the polygons together
var out = PolyBool.segments({ inverted: false, regions: [] });
for (var i = 0; i < geojson.coordinates.length; i++)
out = PolyBool.selectUnion(PolyBool.combine(out, GeoPoly(geojson.coordinates[i])));
return PolyBool.polygon(out);
}
throw new Error('PolyBool: Cannot convert GeoJSON object to PolyBool polygon');
},

// convert a PolyBool polygon to a GeoJSON object
fromPolygon: function(PolyBool, eps, poly){
// make sure out polygon is clean
poly = PolyBool.polygon(PolyBool.segments(poly));

// test if r1 is inside r2
function regionInsideRegion(r1, r2){
// we're guaranteed no lines intersect (because the polygon is clean), but a vertex
// could be on the edge -- so we just average pt[0] and pt[1] to produce a point on the
// edge of the first line, which cannot be on an edge
return eps.pointInsideRegion([
(r1[0][0] + r1[1][0]) * 0.5,
(r1[0][1] + r1[1][1]) * 0.5
], r2);
}

// calculate inside heirarchy
//
// _____________________ _______ roots -> A -> F
// | A | | F | | |
// | _______ _______ | | ___ | +-- B +-- G
// | | B | | C | | | | | | | |
// | | ___ | | ___ | | | | | | | +-- D
// | | | D | | | | E | | | | | G | | |
// | | |___| | | |___| | | | | | | +-- C
// | |_______| |_______| | | |___| | |
// |_____________________| |_______| +-- E

function newNode(region){
return {
region: region,
children: []
};
}

var roots = newNode(null);

function addChild(root, region){
// first check if we're inside any children
for (var i = 0; i < root.children.length; i++){
var child = root.children[i];
if (regionInsideRegion(region, child.region)){
// we are, so insert inside them instead
addChild(child, region);
return;
}
}

// not inside any children, so check to see if any children are inside us
var node = newNode(region);
for (var i = 0; i < root.children.length; i++){
var child = root.children[i];
if (regionInsideRegion(child.region, region)){
// oops... move the child beneath us, and remove them from root
node.children.push(child);
root.children.splice(i, 1);
i--;
}
}

// now we can add ourselves
root.children.push(node);
}

// add all regions to the root
for (var i = 0; i < poly.regions.length; i++){
var region = poly.regions[i];
if (region.length < 3) // regions must have at least 3 points (sanity check)
continue;
addChild(roots, region);
}

// with our heirarchy, we can distinguish between exterior borders, and interior holes
// the root nodes are exterior, children are interior, children's children are exterior,
// children's children's children are interior, etc

// while we're at it, exteriors are counter-clockwise, and interiors are clockwise

function forceWinding(region, clockwise){
// first, see if we're clockwise or counter-clockwise
// https://en.wikipedia.org/wiki/Shoelace_formula
var winding = 0;
var last_x = region[region.length - 1][0];
var last_y = region[region.length - 1][1];
var copy = [];
for (var i = 0; i < region.length; i++){
var curr_x = region[i][0];
var curr_y = region[i][1];
copy.push([curr_x, curr_y]); // create a copy while we're at it
winding += curr_y * last_x - curr_x * last_y;
last_x = curr_x;
last_y = curr_y;
}
// this assumes Cartesian coordinates (Y is positive going up)
var isclockwise = winding < 0;
if (isclockwise !== clockwise)
copy.reverse();
// while we're here, the last point must be the first point...
copy.push([copy[0][0], copy[0][1]]);
return copy;
}

var geopolys = [];

function addExterior(node){
var poly = [forceWinding(node.region, false)];
geopolys.push(poly);
// children of exteriors are interior
for (var i = 0; i < node.children.length; i++)
poly.push(getInterior(node.children[i]));
}

function getInterior(node){
// children of interiors are exterior
for (var i = 0; i < node.children.length; i++)
addExterior(node.children[i]);
// return the clockwise interior
return forceWinding(node.region, true);
}

// root nodes are exterior
for (var i = 0; i < roots.children.length; i++)
addExterior(roots.children[i]);

// lastly, construct the approrpriate GeoJSON object

if (geopolys.length <= 0) // empty GeoJSON Polygon
return { type: 'Polygon', coordinates: [] };
if (geopolys.length == 1) // use a GeoJSON Polygon
return { type: 'Polygon', coordinates: geopolys[0] };
return { // otherwise, use a GeoJSON MultiPolygon
type: 'MultiPolygon',
coordinates: geopolys
};
}
};

module.exports = GeoJSON;

},{}],5:[function(require,module,exports){
// (c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc
// MIT License
// Project Home: https://github.com/voidqk/polybooljs
Expand Down Expand Up @@ -889,7 +1110,7 @@ function Intersecter(selfIntersection, eps, buildLog){

module.exports = Intersecter;

},{"./linked-list":5}],5:[function(require,module,exports){
},{"./linked-list":6}],6:[function(require,module,exports){
// (c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc
// MIT License
// Project Home: https://github.com/voidqk/polybooljs
Expand Down Expand Up @@ -972,7 +1193,7 @@ var LinkedList = {

module.exports = LinkedList;

},{}],6:[function(require,module,exports){
},{}],7:[function(require,module,exports){
// (c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc
// MIT License
// Project Home: https://github.com/voidqk/polybooljs
Expand Down Expand Up @@ -1226,7 +1447,7 @@ function SegmentChainer(segments, eps, buildLog){

module.exports = SegmentChainer;

},{}],7:[function(require,module,exports){
},{}],8:[function(require,module,exports){
// (c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc
// MIT License
// Project Home: https://github.com/voidqk/polybooljs
Expand Down
2 changes: 1 addition & 1 deletion dist/polybool.min.js

Large diffs are not rendered by default.

Loading

0 comments on commit 6e20104

Please sign in to comment.