polygon-offset
Version:
Polygon offsetting algorithm, aimed for use with leaflet
1,876 lines (1,553 loc) • 190 kB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
(function (global){
var Offset = global.Offset = require('../src/offset');
require('./leaflet_multipolygon');
require('./polygon_control');
var OffsetControl = require('./offset_control');
var data = require('../test/fixtures/demo.json');
var project = require('geojson-project');
var arcSegments = 5;
var style = {
weight: 3,
color: '#48f',
opacity: 0.8,
dashArray: [2, 4]
},
marginStyle = {
weight: 2,
color: '#276D8F'
},
paddingStyle = {
weight: 2,
color: '#D81706'
},
center = [22.2670, 114.188],
zoom = 17,
map, vertices, result;
map = global.map = L.map('map', {
editable: true,
maxZoom: 22
}).setView(center, zoom);
map.addControl(new L.NewPolygonControl({
callback: map.editTools.startPolygon
}));
map.addControl(new L.NewLineControl({
callback: map.editTools.startPolyline
}));
map.addControl(new L.NewPointControl({
callback: map.editTools.startMarker
}));
var layers = global.layers = L.geoJson(data).addTo(map);
var results = global.results = L.geoJson(null, {
style: function(feature) {
return marginStyle;
}
}).addTo(map);
map.fitBounds(layers.getBounds(), { animate: false });
map.addControl(new OffsetControl({
clear: function() {
layers.clearLayers();
},
callback: run
}));
map.on('editable:created', function(evt) {
layers.addLayer(evt.layer);
evt.layer.on('click', function(e) {
if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey) && this.editEnabled()) {
this.editor.newHole(e.latlng);
}
});
});
function run (margin) {
results.clearLayers();
layers.eachLayer(function(layer) {
var gj = layer.toGeoJSON();
console.log(gj, margin);
var shape = project(gj, function(coord) {
var pt = map.options.crs.latLngToPoint(L.latLng(coord.slice().reverse()), map.getZoom());
return [pt.x, pt.y];
});
var margined;
console.log(gj.geometry.type);
if (gj.geometry.type === 'LineString') {
if (margin < 0) return;
var res = new Offset(shape.geometry.coordinates)
.arcSegments(arcSegments)
.offsetLine(margin);
margined = {
type: 'Feature',
geometry: {
type: margin === 0 ? 'LineString' : 'Polygon',
coordinates: res
}
};
} else if (gj.geometry.type === 'Point') {
var res = new Offset(shape.geometry.coordinates)
.arcSegments(arcSegments)
.offset(margin);
margined = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: res
}
};
} else {
var res = new Offset(shape.geometry.coordinates).offset(margin);
margined = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: res
}
};
}
console.log('margined', margined);
results.addData(project(margined, function(pt) {
var ll = map.options.crs.pointToLatLng(L.point(pt.slice()), map.getZoom());
return [ll.lng, ll.lat];
}));
});
}
run (20);
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../src/offset":21,"../test/fixtures/demo.json":23,"./leaflet_multipolygon":2,"./offset_control":3,"./polygon_control":4,"geojson-project":9}],2:[function(require,module,exports){
L.Polygon.prototype._projectLatlngs = function (latlngs, result, projectedBounds, isHole) {
var flat = latlngs[0] instanceof L.LatLng,
len = latlngs.length,
i, ring, area;
if (flat) {
area = 0;
ring = [];
for (i = 0; i < len; i++) {
ring[i] = this._map.latLngToLayerPoint(latlngs[i]);
projectedBounds.extend(ring[i]);
if (i) {
area += ring[i - 1].x * ring[i].y;
area -= ring[i].x * ring[i - 1].y;
}
}
area += ring[len - 1].x * ring[0].y;
area -= ring[0].x * ring[len - 1].y;
if ((!isHole && area > 0) || (isHole && area < 0)) {
ring.reverse();
}
result.push(ring);
} else {
for (i = 0; i < len; i++) {
this._projectLatlngs(latlngs[i], result, projectedBounds, i !== 0);
}
}
};
L.Polygon.prototype._project = function() {
L.Polyline.prototype._project.call(this);
if ((this._latlngs.length > 1) &&
!L.Polyline._flat(this._latlngs) &&
!(this._latlngs[0][0] instanceof L.LatLng)) {
if (this.options.fillRule !== 'nonzero') {
this.setStyle({
fillRule: 'nonzero'
});
}
}
};
},{}],3:[function(require,module,exports){
module.exports = L.Control.extend({
options: {
position: 'topright',
defaultMargin: 20
},
onAdd: function(map) {
var container = this._container = L.DomUtil.create('div', 'leaflet-bar');
this._container.style.background = '#ffffff';
this._container.style.padding = '10px';
container.innerHTML = [
'<form>',
'<div>',
'<label>',
'<input type="range" min="0" max="100" value="', this.options.defaultMargin, '" name="margin">',
'</label>',
'</div>',
'<div>',
'<label>', '<input type="radio" name="operation" value="1" checked>', ' margin</label>',
'<label>', '<input type="radio" name="operation" value="-1">', ' padding</label>',
'</div>', '<br>',
'<input type="submit" value="Run">', '<input name="clear" type="button" value="Clear layers">',
'</form>'].join('');
var form = container.querySelector('form');
L.DomEvent
.on(form, 'submit', function (evt) {
L.DomEvent.stop(evt);
var margin = parseFloat(form['margin'].value);
var radios = Array.prototype.slice.call(
form.querySelectorAll('input[type=radio]'));
var k = 1;
for (var i = 0, len = radios.length; i < len; i++) {
if (radios[i].checked) {
k *= parseInt(radios[i].value);
break;
}
}
this.options.callback(margin * k);
}, this)
.on(form['clear'], 'click', function(evt) {
L.DomEvent.stop(evt);
this.options.clear();
}, this);
L.DomEvent
.disableClickPropagation(this._container)
.disableScrollPropagation(this._container);
return this._container;
}
});
},{}],4:[function(require,module,exports){
L.EditControl = L.Control.extend({
options: {
position: 'topleft',
callback: null,
kind: '',
html: ''
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar'),
link = L.DomUtil.create('a', '', container);
link.href = '#';
link.title = 'Create a new ' + this.options.kind;
link.innerHTML = this.options.html;
L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', function () {
window.LAYER = this.options.callback.call(map.editTools);
}, this);
return container;
}
});
L.NewPolygonControl = L.EditControl.extend({
options: {
position: 'topleft',
kind: 'polygon',
html: '∆'
}
});
L.NewLineControl = L.EditControl.extend({
options: {
position: 'topleft',
kind: 'polyline',
html: '/'
}
});
L.NewPointControl = L.EditControl.extend({
options: {
position: 'topleft',
kind: 'point',
html: '●'
}
});
},{}],5:[function(require,module,exports){
module.exports = {
RBTree: require('./lib/rbtree'),
BinTree: require('./lib/bintree')
};
},{"./lib/bintree":6,"./lib/rbtree":7}],6:[function(require,module,exports){
var TreeBase = require('./treebase');
function Node(data) {
this.data = data;
this.left = null;
this.right = null;
}
Node.prototype.get_child = function(dir) {
return dir ? this.right : this.left;
};
Node.prototype.set_child = function(dir, val) {
if(dir) {
this.right = val;
}
else {
this.left = val;
}
};
function BinTree(comparator) {
this._root = null;
this._comparator = comparator;
this.size = 0;
}
BinTree.prototype = new TreeBase();
// returns true if inserted, false if duplicate
BinTree.prototype.insert = function(data) {
if(this._root === null) {
// empty tree
this._root = new Node(data);
this.size++;
return true;
}
var dir = 0;
// setup
var p = null; // parent
var node = this._root;
// search down
while(true) {
if(node === null) {
// insert new node at the bottom
node = new Node(data);
p.set_child(dir, node);
ret = true;
this.size++;
return true;
}
// stop if found
if(this._comparator(node.data, data) === 0) {
return false;
}
dir = this._comparator(node.data, data) < 0;
// update helpers
p = node;
node = node.get_child(dir);
}
};
// returns true if removed, false if not found
BinTree.prototype.remove = function(data) {
if(this._root === null) {
return false;
}
var head = new Node(undefined); // fake tree root
var node = head;
node.right = this._root;
var p = null; // parent
var found = null; // found item
var dir = 1;
while(node.get_child(dir) !== null) {
p = node;
node = node.get_child(dir);
var cmp = this._comparator(data, node.data);
dir = cmp > 0;
if(cmp === 0) {
found = node;
}
}
if(found !== null) {
found.data = node.data;
p.set_child(p.right === node, node.get_child(node.left === null));
this._root = head.right;
this.size--;
return true;
}
else {
return false;
}
};
module.exports = BinTree;
},{"./treebase":8}],7:[function(require,module,exports){
var TreeBase = require('./treebase');
function Node(data) {
this.data = data;
this.left = null;
this.right = null;
this.red = true;
}
Node.prototype.get_child = function(dir) {
return dir ? this.right : this.left;
};
Node.prototype.set_child = function(dir, val) {
if(dir) {
this.right = val;
}
else {
this.left = val;
}
};
function RBTree(comparator) {
this._root = null;
this._comparator = comparator;
this.size = 0;
}
RBTree.prototype = new TreeBase();
// returns true if inserted, false if duplicate
RBTree.prototype.insert = function(data) {
var ret = false;
if(this._root === null) {
// empty tree
this._root = new Node(data);
ret = true;
this.size++;
}
else {
var head = new Node(undefined); // fake tree root
var dir = 0;
var last = 0;
// setup
var gp = null; // grandparent
var ggp = head; // grand-grand-parent
var p = null; // parent
var node = this._root;
ggp.right = this._root;
// search down
while(true) {
if(node === null) {
// insert new node at the bottom
node = new Node(data);
p.set_child(dir, node);
ret = true;
this.size++;
}
else if(is_red(node.left) && is_red(node.right)) {
// color flip
node.red = true;
node.left.red = false;
node.right.red = false;
}
// fix red violation
if(is_red(node) && is_red(p)) {
var dir2 = ggp.right === gp;
if(node === p.get_child(last)) {
ggp.set_child(dir2, single_rotate(gp, !last));
}
else {
ggp.set_child(dir2, double_rotate(gp, !last));
}
}
var cmp = this._comparator(node.data, data);
// stop if found
if(cmp === 0) {
break;
}
last = dir;
dir = cmp < 0;
// update helpers
if(gp !== null) {
ggp = gp;
}
gp = p;
p = node;
node = node.get_child(dir);
}
// update root
this._root = head.right;
}
// make root black
this._root.red = false;
return ret;
};
// returns true if removed, false if not found
RBTree.prototype.remove = function(data) {
if(this._root === null) {
return false;
}
var head = new Node(undefined); // fake tree root
var node = head;
node.right = this._root;
var p = null; // parent
var gp = null; // grand parent
var found = null; // found item
var dir = 1;
while(node.get_child(dir) !== null) {
var last = dir;
// update helpers
gp = p;
p = node;
node = node.get_child(dir);
var cmp = this._comparator(data, node.data);
dir = cmp > 0;
// save found node
if(cmp === 0) {
found = node;
}
// push the red node down
if(!is_red(node) && !is_red(node.get_child(dir))) {
if(is_red(node.get_child(!dir))) {
var sr = single_rotate(node, dir);
p.set_child(last, sr);
p = sr;
}
else if(!is_red(node.get_child(!dir))) {
var sibling = p.get_child(!last);
if(sibling !== null) {
if(!is_red(sibling.get_child(!last)) && !is_red(sibling.get_child(last))) {
// color flip
p.red = false;
sibling.red = true;
node.red = true;
}
else {
var dir2 = gp.right === p;
if(is_red(sibling.get_child(last))) {
gp.set_child(dir2, double_rotate(p, last));
}
else if(is_red(sibling.get_child(!last))) {
gp.set_child(dir2, single_rotate(p, last));
}
// ensure correct coloring
var gpc = gp.get_child(dir2);
gpc.red = true;
node.red = true;
gpc.left.red = false;
gpc.right.red = false;
}
}
}
}
}
// replace and remove if found
if(found !== null) {
found.data = node.data;
p.set_child(p.right === node, node.get_child(node.left === null));
this.size--;
}
// update root and make it black
this._root = head.right;
if(this._root !== null) {
this._root.red = false;
}
return found !== null;
};
function is_red(node) {
return node !== null && node.red;
}
function single_rotate(root, dir) {
var save = root.get_child(!dir);
root.set_child(!dir, save.get_child(dir));
save.set_child(dir, root);
root.red = true;
save.red = false;
return save;
}
function double_rotate(root, dir) {
root.set_child(!dir, single_rotate(root.get_child(!dir), !dir));
return single_rotate(root, dir);
}
module.exports = RBTree;
},{"./treebase":8}],8:[function(require,module,exports){
function TreeBase() {}
// removes all nodes from the tree
TreeBase.prototype.clear = function() {
this._root = null;
this.size = 0;
};
// returns node data if found, null otherwise
TreeBase.prototype.find = function(data) {
var res = this._root;
while(res !== null) {
var c = this._comparator(data, res.data);
if(c === 0) {
return res.data;
}
else {
res = res.get_child(c > 0);
}
}
return null;
};
// returns iterator to node if found, null otherwise
TreeBase.prototype.findIter = function(data) {
var res = this._root;
var iter = this.iterator();
while(res !== null) {
var c = this._comparator(data, res.data);
if(c === 0) {
iter._cursor = res;
return iter;
}
else {
iter._ancestors.push(res);
res = res.get_child(c > 0);
}
}
return null;
};
// Returns an iterator to the tree node at or immediately after the item
TreeBase.prototype.lowerBound = function(item) {
var cur = this._root;
var iter = this.iterator();
var cmp = this._comparator;
while(cur !== null) {
var c = cmp(item, cur.data);
if(c === 0) {
iter._cursor = cur;
return iter;
}
iter._ancestors.push(cur);
cur = cur.get_child(c > 0);
}
for(var i=iter._ancestors.length - 1; i >= 0; --i) {
cur = iter._ancestors[i];
if(cmp(item, cur.data) < 0) {
iter._cursor = cur;
iter._ancestors.length = i;
return iter;
}
}
iter._ancestors.length = 0;
return iter;
};
// Returns an iterator to the tree node immediately after the item
TreeBase.prototype.upperBound = function(item) {
var iter = this.lowerBound(item);
var cmp = this._comparator;
while(iter.data() !== null && cmp(iter.data(), item) === 0) {
iter.next();
}
return iter;
};
// returns null if tree is empty
TreeBase.prototype.min = function() {
var res = this._root;
if(res === null) {
return null;
}
while(res.left !== null) {
res = res.left;
}
return res.data;
};
// returns null if tree is empty
TreeBase.prototype.max = function() {
var res = this._root;
if(res === null) {
return null;
}
while(res.right !== null) {
res = res.right;
}
return res.data;
};
// returns a null iterator
// call next() or prev() to point to an element
TreeBase.prototype.iterator = function() {
return new Iterator(this);
};
// calls cb on each node's data, in order
TreeBase.prototype.each = function(cb) {
var it=this.iterator(), data;
while((data = it.next()) !== null) {
cb(data);
}
};
// calls cb on each node's data, in reverse order
TreeBase.prototype.reach = function(cb) {
var it=this.iterator(), data;
while((data = it.prev()) !== null) {
cb(data);
}
};
function Iterator(tree) {
this._tree = tree;
this._ancestors = [];
this._cursor = null;
}
Iterator.prototype.data = function() {
return this._cursor !== null ? this._cursor.data : null;
};
// if null-iterator, returns first node
// otherwise, returns next node
Iterator.prototype.next = function() {
if(this._cursor === null) {
var root = this._tree._root;
if(root !== null) {
this._minNode(root);
}
}
else {
if(this._cursor.right === null) {
// no greater node in subtree, go up to parent
// if coming from a right child, continue up the stack
var save;
do {
save = this._cursor;
if(this._ancestors.length) {
this._cursor = this._ancestors.pop();
}
else {
this._cursor = null;
break;
}
} while(this._cursor.right === save);
}
else {
// get the next node from the subtree
this._ancestors.push(this._cursor);
this._minNode(this._cursor.right);
}
}
return this._cursor !== null ? this._cursor.data : null;
};
// if null-iterator, returns last node
// otherwise, returns previous node
Iterator.prototype.prev = function() {
if(this._cursor === null) {
var root = this._tree._root;
if(root !== null) {
this._maxNode(root);
}
}
else {
if(this._cursor.left === null) {
var save;
do {
save = this._cursor;
if(this._ancestors.length) {
this._cursor = this._ancestors.pop();
}
else {
this._cursor = null;
break;
}
} while(this._cursor.left === save);
}
else {
this._ancestors.push(this._cursor);
this._maxNode(this._cursor.left);
}
}
return this._cursor !== null ? this._cursor.data : null;
};
Iterator.prototype._minNode = function(start) {
while(start.left !== null) {
this._ancestors.push(start);
start = start.left;
}
this._cursor = start;
};
Iterator.prototype._maxNode = function(start) {
while(start.right !== null) {
this._ancestors.push(start);
start = start.right;
}
this._cursor = start;
};
module.exports = TreeBase;
},{}],9:[function(require,module,exports){
/**
* Node & browser script to transform/project geojson coordinates
* @copyright Alexander Milevski <info@w8r.name>
* @preserve
* @license MIT
*/
(function (factory) { // UMD wrapper
if (typeof define === 'function' && define.amd) { // AMD
define(factory);
} else if (typeof module === 'object' &&
typeof module.exports === "object") { // Node/CommonJS
module.exports = factory();
} else { // Browser globals
window.geojsonProject = factory();
}
})(function () {
/**
* Takes in GeoJSON and applies a function to each coordinate,
* with a given context
*
* @param {Object} data GeoJSON
* @param {Function} project
* @param {*=} context
* @return {Object}
*/
function geojsonProject (data, project, context) {
data = JSON.parse(JSON.stringify(data));
if (data.type === 'FeatureCollection') {
// That's a huge hack to get things working with both ArcGIS server
// and GeoServer. Geoserver provides crs reference in GeoJSON, ArcGIS —
// doesn't.
//if (data.crs) delete data.crs;
for (var i = data.features.length - 1; i >= 0; i--) {
data.features[i] = projectFeature(data.features[i], project, context);
}
} else {
data = projectFeature(data, project, context);
}
return data;
};
geojsonProject.projectFeature = projectFeature;
geojsonProject.projectGeometry = projectGeometry;
/**
* @param {Object} data GeoJSON
* @param {Function} project
* @param {*=} context
* @return {Object}
*/
function projectFeature (feature, project, context) {
if (feature.geometry.type === 'GeometryCollection') {
for (var i = 0, len = feature.geometry.geometries.length; i < len; i++) {
feature.geometry.geometries[i] =
projectGeometry(feature.geometry.geometries[i], project, context);
}
} else {
feature.geometry = projectGeometry(feature.geometry, project, context);
}
return feature;
}
/**
* @param {Object} data GeoJSON
* @param {Function} project
* @param {*=} context
* @return {Object}
*/
function projectGeometry (geometry, project, context) {
var coords = geometry.coordinates;
switch (geometry.type) {
case 'Point':
geometry.coordinates = project.call(context, coords);
break;
case 'MultiPoint':
case 'LineString':
for (var i = 0, len = coords.length; i < len; i++) {
coords[i] = project.call(context, coords[i]);
}
geometry.coordinates = coords;
break;
case 'Polygon':
geometry.coordinates = projectCoords(coords, 1, project, context);
break;
case 'MultiLineString':
geometry.coordinates = projectCoords(coords, 1, project, context);
break;
case 'MultiPolygon':
geometry.coordinates = projectCoords(coords, 2, project, context);
break;
default:
break;
}
return geometry;
}
/**
* @param {*} coords Coords arrays
* @param {Number} levelsDeep
* @param {Function} project
* @param {*=} context
* @return {*}
*/
function projectCoords (coords, levelsDeep, project, context) {
var coord, i, len;
var result = [];
for (i = 0, len = coords.length; i < len; i++) {
coord = levelsDeep ?
projectCoords(coords[i], levelsDeep - 1, project, context) :
project.call(context, coords[i]);
result.push(coord);
}
return result;
}
return geojsonProject;
});
},{}],10:[function(require,module,exports){
module.exports = require('./src/index');
},{"./src/index":15}],11:[function(require,module,exports){
var signedArea = require('./signed_area');
// var equals = require('./equals');
/**
* @param {SweepEvent} e1
* @param {SweepEvent} e2
* @return {Number}
*/
module.exports = function sweepEventsComp(e1, e2) {
var p1 = e1.point;
var p2 = e2.point;
// Different x-coordinate
if (p1[0] > p2[0]) return 1;
if (p1[0] < p2[0]) return -1;
// Different points, but same x-coordinate
// Event with lower y-coordinate is processed first
if (p1[1] !== p2[1]) return p1[1] > p2[1] ? 1 : -1;
return specialCases(e1, e2, p1, p2);
};
function specialCases(e1, e2, p1, p2) {
// Same coordinates, but one is a left endpoint and the other is
// a right endpoint. The right endpoint is processed first
if (e1.left !== e2.left)
return e1.left ? 1 : -1;
// Same coordinates, both events
// are left endpoints or right endpoints.
// not collinear
if (signedArea (p1, e1.otherEvent.point, e2.otherEvent.point) !== 0) {
// the event associate to the bottom segment is processed first
return (!e1.isBelow(e2.otherEvent.point)) ? 1 : -1;
}
// uncomment this if you want to play with multipolygons
// if (e1.isSubject === e2.isSubject) {
// if(equals(e1.point, e2.point) && e1.contourId === e2.contourId) {
// return 0;
// } else {
// return e1.contourId > e2.contourId ? 1 : -1;
// }
// }
return (!e1.isSubject && e2.isSubject) ? 1 : -1;
}
},{"./signed_area":17}],12:[function(require,module,exports){
var signedArea = require('./signed_area');
var compareEvents = require('./compare_events');
var equals = require('./equals');
/**
* @param {SweepEvent} le1
* @param {SweepEvent} le2
* @return {Number}
*/
module.exports = function compareSegments(le1, le2) {
if (le1 === le2) return 0;
// Segments are not collinear
if (signedArea(le1.point, le1.otherEvent.point, le2.point) !== 0 ||
signedArea(le1.point, le1.otherEvent.point, le2.otherEvent.point) !== 0) {
// If they share their left endpoint use the right endpoint to sort
if (equals(le1.point, le2.point)) return le1.isBelow(le2.otherEvent.point) ? -1 : 1;
// Different left endpoint: use the left endpoint to sort
if (le1.point[0] === le2.point[0]) return le1.point[1] < le2.point[1] ? -1 : 1;
// has the line segment associated to e1 been inserted
// into S after the line segment associated to e2 ?
if (compareEvents(le1, le2) === 1) return le2.isAbove(le1.point) ? -1 : 1;
// The line segment associated to e2 has been inserted
// into S after the line segment associated to e1
return le1.isBelow(le2.point) ? -1 : 1;
}
if (le1.isSubject === le2.isSubject) { // same polygon
if (equals(le1.point, le2.point)) {
if (equals(le1.otherEvent.point, le2.otherEvent.point)) {
return 0;
} else {
return le1.contourId > le2.contourId ? 1 : -1;
}
}
} else { // Segments are collinear, but belong to separate polygons
return le1.isSubject ? -1 : 1;
}
return compareEvents(le1, le2) === 1 ? 1 : -1;
};
},{"./compare_events":11,"./equals":14,"./signed_area":17}],13:[function(require,module,exports){
module.exports = {
NORMAL: 0,
NON_CONTRIBUTING: 1,
SAME_TRANSITION: 2,
DIFFERENT_TRANSITION: 3
};
},{}],14:[function(require,module,exports){
module.exports = function equals(p1, p2) {
return p1[0] === p2[0] && p1[1] === p2[1];
};
},{}],15:[function(require,module,exports){
var INTERSECTION = 0;
var UNION = 1;
var DIFFERENCE = 2;
var XOR = 3;
var EMPTY = [];
var edgeType = require('./edge_type');
var Queue = require('tinyqueue');
var Tree = require('bintrees').RBTree;
var SweepEvent = require('./sweep_event');
var compareEvents = require('./compare_events');
var compareSegments = require('./compare_segments');
var intersection = require('./segment_intersection');
var equals = require('./equals');
var max = Math.max;
var min = Math.min;
// global.Tree = Tree;
// global.compareSegments = compareSegments;
// global.SweepEvent = SweepEvent;
// global.signedArea = require('./signed_area');
/**
* @param {<Array.<Number>} s1
* @param {<Array.<Number>} s2
* @param {Boolean} isSubject
* @param {Queue} eventQueue
* @param {Array.<Number>} bbox
*/
function processSegment(s1, s2, isSubject, depth, eventQueue, bbox) {
// Possible degenerate condition.
// if (equals(s1, s2)) return;
var e1 = new SweepEvent(s1, false, undefined, isSubject);
var e2 = new SweepEvent(s2, false, e1, isSubject);
e1.otherEvent = e2;
e1.contourId = e2.contourId = depth;
if (compareEvents(e1, e2) > 0) {
e2.left = true;
} else {
e1.left = true;
}
bbox[0] = min(bbox[0], s1[0]);
bbox[1] = min(bbox[1], s1[1]);
bbox[2] = max(bbox[2], s1[0]);
bbox[3] = max(bbox[3], s1[1]);
// Pushing it so the queue is sorted from left to right,
// with object on the left having the highest priority.
eventQueue.push(e1);
eventQueue.push(e2);
}
var contourId = 0;
function processPolygon(polygon, isSubject, depth, queue, bbox) {
var i, len;
if (typeof polygon[0][0] === 'number') {
for (i = 0, len = polygon.length - 1; i < len; i++) {
processSegment(polygon[i], polygon[i + 1], isSubject, depth + 1, queue, bbox);
}
} else {
for (i = 0, len = polygon.length; i < len; i++) {
contourId++;
processPolygon(polygon[i], isSubject, contourId, queue, bbox);
}
}
}
function fillQueue(subject, clipping, sbbox, cbbox) {
var eventQueue = new Queue(null, compareEvents);
contourId = 0;
processPolygon(subject, true, 0, eventQueue, sbbox);
processPolygon(clipping, false, 0, eventQueue, cbbox);
return eventQueue;
}
function computeFields(event, prev, sweepLine, operation) {
// compute inOut and otherInOut fields
if (prev === null) {
event.inOut = false;
event.otherInOut = true;
// previous line segment in sweepline belongs to the same polygon
} else if (event.isSubject === prev.isSubject) {
event.inOut = !prev.inOut;
event.otherInOut = prev.otherInOut;
// previous line segment in sweepline belongs to the clipping polygon
} else {
event.inOut = !prev.otherInOut;
event.otherInOut = prev.isVertical() ? !prev.inOut : prev.inOut;
}
// compute prevInResult field
if (prev) {
event.prevInResult = (!inResult(prev, operation) || prev.isVertical()) ?
prev.prevInResult : prev;
}
// check if the line segment belongs to the Boolean operation
event.inResult = inResult(event, operation);
}
function inResult(event, operation) {
switch (event.type) {
case edgeType.NORMAL:
switch (operation) {
case INTERSECTION:
return !event.otherInOut;
case UNION:
return event.otherInOut;
case DIFFERENCE:
return (event.isSubject && event.otherInOut) ||
(!event.isSubject && !event.otherInOut);
case XOR:
return true;
}
case edgeType.SAME_TRANSITION:
return operation === INTERSECTION || operation === UNION;
case edgeType.DIFFERENT_TRANSITION:
return operation === DIFFERENCE;
case edgeType.NON_CONTRIBUTING:
return false;
}
return false;
}
/**
* @param {SweepEvent} se1
* @param {SweepEvent} se2
* @param {Queue} queue
* @return {Number}
*/
function possibleIntersection(se1, se2, queue) {
// that disallows self-intersecting polygons,
// did cost us half a day, so I'll leave it
// out of respect
// if (se1.isSubject === se2.isSubject) return;
var inter = intersection(
se1.point, se1.otherEvent.point,
se2.point, se2.otherEvent.point
);
var nintersections = inter ? inter.length : 0;
if (nintersections === 0) return 0; // no intersection
// the line segments intersect at an endpoint of both line segments
if ((nintersections === 1) &&
(equals(se1.point, se2.point) ||
equals(se1.otherEvent.point, se2.otherEvent.point))) {
return 0;
}
if (nintersections === 2 && se1.isSubject === se2.isSubject){
if(se1.contourId === se2.contourId){
console.warn('Edges of the same polygon overlap',
se1.point, se1.otherEvent.point, se2.point, se2.otherEvent.point);
}
//throw new Error('Edges of the same polygon overlap');
return 0;
}
// The line segments associated to se1 and se2 intersect
if (nintersections === 1) {
// if the intersection point is not an endpoint of se1
if (!equals(se1.point, inter[0]) && !equals(se1.otherEvent.point, inter[0])) {
divideSegment(se1, inter[0], queue);
}
// if the intersection point is not an endpoint of se2
if (!equals(se2.point, inter[0]) && !equals(se2.otherEvent.point, inter[0])) {
divideSegment(se2, inter[0], queue);
}
return 1;
}
// The line segments associated to se1 and se2 overlap
var events = [];
var leftCoincide = false;
var rightCoincide = false;
if (equals(se1.point, se2.point)) {
leftCoincide = true; // linked
} else if (compareEvents(se1, se2) === 1) {
events.push(se2, se1);
} else {
events.push(se1, se2);
}
if (equals(se1.otherEvent.point, se2.otherEvent.point)) {
rightCoincide = true;
} else if (compareEvents(se1.otherEvent, se2.otherEvent) === 1) {
events.push(se2.otherEvent, se1.otherEvent);
} else {
events.push(se1.otherEvent, se2.otherEvent);
}
if ((leftCoincide && rightCoincide) || leftCoincide) {
// both line segments are equal or share the left endpoint
se1.type = edgeType.NON_CONTRIBUTING;
se2.type = (se1.inOut === se2.inOut) ?
edgeType.SAME_TRANSITION :
edgeType.DIFFERENT_TRANSITION;
if (leftCoincide && !rightCoincide) {
// honestly no idea, but changing events selection from [2, 1]
// to [0, 1] fixes the overlapping self-intersecting polygons issue
divideSegment(events[0].otherEvent, events[1].point, queue);
}
return 2;
}
// the line segments share the right endpoint
if (rightCoincide) {
divideSegment(events[0], events[1].point, queue);
return 3;
}
// no line segment includes totally the other one
if (events[0] !== events[3].otherEvent) {
divideSegment(events[0], events[1].point, queue);
divideSegment(events[1], events[2].point, queue);
return 3;
}
// one line segment includes the other one
divideSegment(events[0], events[1].point, queue);
divideSegment(events[3].otherEvent, events[2].point, queue);
return 3;
}
/**
* @param {SweepEvent} se
* @param {Array.<Number>} p
* @param {Queue} queue
* @return {Queue}
*/
function divideSegment(se, p, queue) {
var r = new SweepEvent(p, false, se, se.isSubject);
var l = new SweepEvent(p, true, se.otherEvent, se.isSubject);
if (equals(se.point, se.otherEvent.point)) {
console.warn('what is that?', se);
}
r.contourId = l.contourId = se.contourId;
// avoid a rounding error. The left event would be processed after the right event
if (compareEvents(l, se.otherEvent) > 0) {
se.otherEvent.left = true;
l.left = false;
}
// avoid a rounding error. The left event would be processed after the right event
// if (compareEvents(se, r) > 0) {}
se.otherEvent.otherEvent = l;
se.otherEvent = r;
queue.push(l);
queue.push(r);
return queue;
}
/* eslint-disable no-unused-vars, no-debugger */
function iteratorEquals(it1, it2) {
return it1._cursor === it2._cursor;
}
function _renderSweepLine(sweepLine, pos, event) {
var map = window.map;
if (!map) return;
if (window.sws) window.sws.forEach(function(p) {
map.removeLayer(p);
});
window.sws = [];
sweepLine.each(function(e) {
var poly = L.polyline([e.point.slice().reverse(), e.otherEvent.point.slice().reverse()], { color: 'green' }).addTo(map);
window.sws.push(poly);
});
if (window.vt) map.removeLayer(window.vt);
var v = pos.slice();
var b = map.getBounds();
window.vt = L.polyline([[b.getNorth(), v[0]], [b.getSouth(), v[0]]], {color: 'green', weight: 1}).addTo(map);
if (window.ps) map.removeLayer(window.ps);
window.ps = L.polyline([event.point.slice().reverse(), event.otherEvent.point.slice().reverse()], {color: 'black', weight: 9, opacity: 0.4}).addTo(map);
debugger;
}
/* eslint-enable no-unused-vars, no-debugger */
function subdivideSegments(eventQueue, subject, clipping, sbbox, cbbox, operation) {
var sortedEvents = [];
var prev, next;
var sweepLine = new Tree(compareSegments);
var sortedEvents = [];
var rightbound = min(sbbox[2], cbbox[2]);
var prev, next;
while (eventQueue.length) {
var event = eventQueue.pop();
sortedEvents.push(event);
// optimization by bboxes for intersection and difference goes here
if ((operation === INTERSECTION && event.point[0] > rightbound) ||
(operation === DIFFERENCE && event.point[0] > sbbox[2])) {
break;
}
if (event.left) {
sweepLine.insert(event);
// _renderSweepLine(sweepLine, event.point, event);
next = sweepLine.findIter(event);
prev = sweepLine.findIter(event);
event.iterator = sweepLine.findIter(event);
// Cannot get out of the tree what we just put there
if (!prev || !next) {
console.log('brute');
var iterators = findIterBrute(sweepLine);
prev = iterators[0];
next = iterators[1];
}
if (prev.data() !== sweepLine.min()) {
prev.prev();
} else {
prev = sweepLine.iterator(); //findIter(sweepLine.max());
prev.prev();
prev.next();
}
next.next();
computeFields(event, prev.data(), sweepLine, operation);
if (next.data()) {
if (possibleIntersection(event, next.data(), eventQueue) === 2) {
computeFields(event, prev.data(), sweepLine, operation);
computeFields(event, next.data(), sweepLine, operation);
}
}
if (prev.data()) {
if (possibleIntersection(prev.data(), event, eventQueue) === 2) {
var prevprev = sweepLine.findIter(prev.data());
if (prevprev.data() !== sweepLine.min()) {
prevprev.prev();
} else {
prevprev = sweepLine.findIter(sweepLine.max());
prevprev.next();
}
computeFields(prev.data(), prevprev.data(), sweepLine, operation);
computeFields(event, prev.data(), sweepLine, operation);
}
}
} else {
event = event.otherEvent;
next = sweepLine.findIter(event);
prev = sweepLine.findIter(event);
// _renderSweepLine(sweepLine, event.otherEvent.point, event);
if (!(prev && next)) continue;
if (prev.data() !== sweepLine.min()) {
prev.prev();
} else {
prev = sweepLine.iterator();
prev.prev(); // sweepLine.findIter(sweepLine.max());
prev.next();
}
next.next();
sweepLine.remove(event);
//_renderSweepLine(sweepLine, event.otherEvent.point, event);
if (next.data() && prev.data()) {
possibleIntersection(prev.data(), next.data(), eventQueue);
}
}
}
return sortedEvents;
}
function findIterBrute(sweepLine, q) {
var prev = sweepLine.iterator();
var next = sweepLine.iterator();
var it = sweepLine.iterator(), data;
while((data = it.next()) !== null) {
prev.next();
next.next();
if (data === event) {
break;
}
}
return [prev, next];
}
function swap (arr, i, n) {
var temp = arr[i];
arr[i] = arr[n];
arr[n] = temp;
}
function changeOrientation(contour) {
return contour.reverse();
}
function isArray (arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
function addHole(contour, idx) {
if (isArray(contour[0]) && !isArray(contour[0][0])) {
contour = [contour];
}
contour[idx] = [];
return contour;
}
/**
* @param {Array.<SweepEvent>} sortedEvents
* @return {Array.<SweepEvent>}
*/
function orderEvents(sortedEvents) {
var event, i, len;
var resultEvents = [];
for (i = 0, len = sortedEvents.length; i < len; i++) {
event = sortedEvents[i];
if ((event.left && event.inResult) ||
(!event.left && event.otherEvent.inResult)) {
resultEvents.push(event);
}
}
// Due to overlapping edges the resultEvents array can be not wholly sorted
var sorted = false;
while (!sorted) {
sorted = true;
for (i = 0, len = resultEvents.length; i < len; i++) {
if ((i + 1) < len &&
compareEvents(resultEvents[i], resultEvents[i + 1]) === 1) {
swap(resultEvents, i, i + 1);
sorted = false;
}
}
}
for (i = 0, len = resultEvents.length; i < len; i++) {
resultEvents[i].pos = i;
}
for (i = 0, len = resultEvents.length; i < len; i++) {
if (!resultEvents[i].left) {
var temp = resultEvents[i].pos;
resultEvents[i].pos = resultEvents[i].otherEvent.pos;
resultEvents[i].otherEvent.pos = temp;
}
}
return resultEvents;
}
/**
* @param {Array.<SweepEvent>} sortedEvents
* @return {Array.<*>} polygons
*/
function connectEdges(sortedEvents) {
var i, len;
var resultEvents = orderEvents(sortedEvents);
// "false"-filled array
var processed = Array(resultEvents.length);
var result = [];
var depth = [];
var holeOf = [];
var isHole = {};
for (i = 0, len = resultEvents.length; i < len; i++) {
if (processed[i]) continue;
var contour = [];
result.push(contour);
var ringId = result.length - 1;
depth.push(0);
holeOf.push(-1);
if (resultEvents[i].prevInResult) {
var lowerContourId = resultEvents[i].prevInResult.contourId;
if (!resultEvents[i].prevInResult.resultInOut) {
addHole(result[lowerContourId], ringId);
holeOf[ringId] = lowerContourId;
depth[ringId] = depth[lowerContourId] + 1;
isHole[ringId] = true;
} else if (isHole[lowerContourId]) {
addHole(result[holeOf[lowerContourId]], ringId);
holeOf[ringId] = holeOf[lowerContourId];
depth[ringId] = depth[lowerContourId];
isHole[ringId] = true;
}
}
var pos = i;
var initial = resultEvents[i].point;
contour.push(initial);
while (pos >= i) {
processed[pos] = true;
if (resultEvents[pos].left) {
resultEvents[pos].resultInOut = false;
resultEvents[pos].contourId = ringId;
} else {
resultEvents[pos].otherEvent.resultInOut = true;
resultEvents[pos].otherEvent.contourId = ringId;
}
pos = resultEvents[pos].pos;
processed[pos] = true;
contour.push(resultEvents[pos].point);
pos = nextPos(pos, resultEvents, processed);
}
pos = pos === -1 ? i : pos;
processed[pos] = processed[resultEvents[pos].pos] = true;
resultEvents[pos].otherEvent.resultInOut = true;
resultEvents[pos].otherEvent.contourId = ringId;
// depth is even
/* eslint-disable no-bitwise */
if (depth[ringId] & 1) {
changeOrientation(contour);
}
/* eslint-enable no-bitwise */
}
return result;
}
/**
* @param {Number} pos
* @param {Array.<SweepEvent>} resultEvents
* @param {Array.<Boolean>} processed
* @return {Number}
*/
function nextPos(pos, resultEvents, processed) {
var newPos = pos + 1;
var length = resultEvents.length;
while (newPos < length &&
equals(resultEvents[newPos].point, resultEvents[pos].point)) {
if (!processed[newPos]) {
return newPos;
} else {
newPos = newPos + 1;
}
}
newPos = pos - 1;
while (processed[newPos]) {
newPos = newPos - 1;
}
return newPos;
}
function trivialOperation(subject, clipping, operation) {
var result = null;
if (subject.length * clipping.length === 0) {
if (operation === INTERSECTION) {
result = EMPTY;
} else if (operation === DIFFERENCE) {
result = subject;
} else if (operation === UNION || operation === XOR) {
result = (subject.length === 0) ? clipping : subject;
}
}
return result;
}
function compareBBoxes(subject, clipping, sbbox, cbbox, operation) {
var result = null;
if (sbbox[0] > cbbox[2] ||
cbbox[0] > sbbox[2] ||
sbbox[1] > cbbox[3] ||
cbbox[1] > sbbox[3]) {
if (operation === INTERSECTION) {
result = EMPTY;
} else if (operation === DIFFERENCE) {
result = subject;
} else if (operation === UNION || operation === XOR) {
result = subject.concat(clipping);
}
}
return result;
}
function boolean(subject, clipping, operation) {
var trivial = trivialOperation(subject, clipping, operation);
if (trivial) {
return trivial === EMPTY ? null : trivial;
}
var sbbox = [Infinity, Infinity, -Infinity, -Infinity];
var cbbox = [Infinity, Infinity, -Infinity, -Infinity];
var eventQueue = fillQueue(subject, clipping, sbbox, cbbox);
trivial = compareBBoxes(subject, clipping, sbbox, cbbox, operation);
if (trivial) {
return trivial === EMPTY ? null : trivial;
}
var sortedEvents = subdivideSegments(eventQueue, subject, clipping, sbbox, cbbox, operation);
return connectEdges(sortedEvents);
}
module.exports = boolean;
module.exports.union = function(subject, clipping) {
return boolean(subject, clipping, UNION);
};
module.exports.diff = function(subject, clipping) {
return boolean(subject, clipping, DIFFERENCE);
};
module.exports.xor = function(subject, clipping) {
return boolean(subject, clipping, XOR);
};
module.exports.intersection = function(subject, clipping) {
return boolean(subject, clipping, INTERSECTION);
};
/**
* @enum {Number}
*/
module.exports.operations = {
INTERSECTION: INTERSECTION,
DIFFERENCE: DIFFERENCE,
UNION: UNION,
XOR: XOR
};
// for testing
module.exports.fillQueue = fillQueue;
module.exports.computeFields = computeFields;
module.exports.subdivideSegments = subdivideSegments;
module.exports.divideSegment = divideSegment;
module.exports.possibleIntersection = possibleIntersection;
},{"./compare_events":11,"./compare_segments":12,"./edge_type":13,"./equals":14,"./segment_intersection":16,"./sweep_event":18,"bintrees":5,"tinyqueue":19}],16:[function(require,module,exports){
var EPSILON = 1e-9;
/**
* Finds the magnitude of the cross product of two vectors (if we pretend
* they're in three dimensions)
*
* @param {Object} a First vector
* @param {Object} b Second vector
* @private
* @returns {Number} The magnitude of the cross product
*/
function krossProduct(a, b) {
return a[0] * b[1] - a[1] * b[0];
}
/**
* Finds the dot product of two vectors.
*
* @param {Object} a First vector
* @param {Object} b Second vector
* @private
* @returns {Number} The dot product
*/
function dotProduct(a, b) {
return a[0] * b[0] + a[1] * b[1];
}
/**
* Finds the intersection (if any) between two line segments a and b, given the
* line segments' end points a1, a2 and b1, b2.
*
* This algorithm is based on Schneider and Eberly.
* http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf
* Page 244.
*
* @param {Array.<Number>} a1 point of first line
* @param {Array.<Number>} a2 point of first line
* @param {Array.<Number>} b1 point of second line
* @param {Array.<Number>} b2 point of second line
* @param {Boolean=} noEndpointTouch whether to skip single touchpoints
* (meaning connected segments) as
* intersections
* @returns {Array.<Array.<Number>>|Null} If the lines intersect, the point of
* intersection. If they overlap, the two end points of the overlapping segment.
* Otherwise, null.
*/
module.exports = function(a1, a2, b1, b2, noEndpointTouch) {
// The algorithm expects our lines in the form P + sd, where P is a point,
// s is on the interval [0, 1], and d is a vector.
// We are passed two points. P can be the first point of each pair. The
// vector, then, could be thought of as the distance (in x and y components)
// from the first point to the second point.
// So first, let's make our vectors:
var va = [a2[0] - a1[0], a2[1] - a1[1]];
var vb = [b2[0] - b1[0], b2[1] - b1[1]];
// We also define a function to convert back to regular point form:
/* eslint-disable arrow-body-style */
function toPoint(p, s, d) {
return [
p[0] + s * d[0],
p[1] + s * d[1]
];
}
/* eslint-enable arrow-body-style */
// The rest is pretty much a straight port of the algorithm.
var e = [b1[0] - a1[0], b1[1] - a1[1]];
var kross = krossProduct(va, vb);
var sqrKross = kross * kross;
var sqrLenA = dotProduct(va, va);
var sqrLenB = dotProduct(vb, vb);
// Check for line intersection. This works because of the properties of the
// cross product -- specifically, two vectors are parallel if and only if the
// cross product is the 0 vector. The full calculation involves relative error
// to ac