polygon-offset
Version:
Polygon offsetting algorithm, aimed for use with leaflet
1,958 lines (1,598 loc) • 59.1 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Offset = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
module.exports = {
RBTree: require('./lib/rbtree'),
BinTree: require('./lib/bintree')
};
},{"./lib/bintree":2,"./lib/rbtree":3}],2:[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":4}],3:[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":4}],4:[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) {
if(cb(data) === false) {
return;
}
}
};
// 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) {
if(cb(data) === false) {
return;
}
}
};
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;
},{}],5:[function(require,module,exports){
module.exports = require('./src/index');
},{"./src/index":10}],6:[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":12}],7:[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":6,"./equals":9,"./signed_area":12}],8:[function(require,module,exports){
module.exports = {
NORMAL: 0,
NON_CONTRIBUTING: 1,
SAME_TRANSITION: 2,
DIFFERENT_TRANSITION: 3
};
},{}],9:[function(require,module,exports){
module.exports = function equals(p1, p2) {
return p1[0] === p2[0] && p1[1] === p2[1];
};
},{}],10:[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":6,"./compare_segments":7,"./edge_type":8,"./equals":9,"./segment_intersection":11,"./sweep_event":13,"bintrees":1,"tinyqueue":14}],11:[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 account for possible very small line segments. See Schneider & Eberly
// for details.
if (sqrKross > EPSILON * sqrLenA * sqrLenB) {
// If they're not parallel, then (because these are line segments) they
// still might not actually intersect. This code checks that the
// intersection point of the lines is actually on both line segments.
var s = krossProduct(e, vb) / kross;
if (s < 0 || s > 1) {
// not on line segment a
return null;
}
var t = krossProduct(e, va) / kross;
if (t < 0 || t > 1) {
// not on line segment b
return null;
}
return noEndpointTouch ? null : [toPoint(a1, s, va)];
}
// If we've reached this point, then the lines are either parallel or the
// same, but the segments could overlap partially or fully, or not at all.
// So we need to find the overlap, if any. To do that, we can use e, which is
// the (vector) difference between the two initial points. If this is parallel
// with the line itself, then the two lines are the same line, and there will
// be overlap.
var sqrLenE = dotProduct(e, e);
kross = krossProduct(e, va);
sqrKross = kross * kross;
if (sqrKross > EPSILON * sqrLenA * sqrLenE) {
// Lines are just parallel, not the same. No overlap.
return null;
}
var sa = dotProduct(va, e) / sqrLenA;
var sb = sa + dotProduct(va, vb) / sqrLenA;
var smin = Math.min(sa, sb);
var smax = Math.max(sa, sb);
// this is, essentially, the FindIntersection acting on floats from
// Schneider & Eberly, just inlined into this function.
if (smin <= 1 && smax >= 0) {
// overlap on an end point
if (smin === 1) {
return noEndpointTouch ? null : [toPoint(a1, smin > 0 ? smin : 0, va)];
}
if (smax === 0) {
return noEndpointTouch ? null : [toPoint(a1, smax < 1 ? smax : 1, va)];
}
if (noEndpointTouch && smin === 0 && smax === 1) return null;
// There's overlap on a segment -- two points of intersection. Return both.
return [
toPoint(a1, smin > 0 ? smin : 0, va),
toPoint(a1, smax < 1 ? smax : 1, va),
];
}
return null;
};
},{}],12:[function(require,module,exports){
/**
* Signed area of the triangle (p0, p1, p2)
* @param {Array.<Number>} p0
* @param {Array.<Number>} p1
* @param {Array.<Number>} p2
* @return {Number}
*/
module.exports = function signedArea(p0, p1, p2) {
return (p0[0] - p2[0]) * (p1[1] - p2[1]) - (p1[0] - p2[0]) * (p0[1] - p2[1]);
};
},{}],13:[function(require,module,exports){
var signedArea = require('./signed_area');
var EdgeType = require('./edge_type');
/**
* Sweepline event
*
* @param {Array.<Number>} point
* @param {Boolean} left
* @param {SweepEvent=} otherEvent
* @param {Boolean} isSubject
* @param {Number} edgeType
*/
function SweepEvent(point, left, otherEvent, isSubject, edgeType) {
/**
* Is left endpoint?
* @type {Boolean}
*/
this.left = left;
/**
* @type {Array.<Number>}
*/
this.point = point;
/**
* Other edge reference
* @type {SweepEvent}
*/
this.otherEvent = otherEvent;
/**
* Belongs to source or clipping polygon
* @type {Boolean}
*/
this.isSubject = isSubject;
/**
* Edge contribution type
* @type {Number}
*/
this.type = edgeType || EdgeType.NORMAL;
/**
* In-out transition for the sweepline crossing polygon
* @type {Boolean}
*/
this.inOut = false;
/**
* @type {Boolean}
*/
this.otherInOut = false;
/**
* Previous event in result?
* @type {SweepEvent}
*/
this.prevInResult = null;
/**
* Does event belong to result?
* @type {Boolean}
*/
this.inResult = false;
// connection step
/**
* @type {Boolean}
*/
this.resultInOut = false;
}
SweepEvent.prototype = {
/**
* @param {Array.<Number>} p
* @return {Boolean}
*/
isBelow: function(p) {
return this.left ?
signedArea (this.point, this.otherEvent.point, p) > 0 :
signedArea (this.otherEvent.point, this.point, p) > 0;
},
/**
* @param {Array.<Number>} p
* @return {Boolean}
*/
isAbove: function(p) {
return !this.isBelow(p);
},
/**
* @return {Boolean}
*/
isVertical: function() {
return this.point[0] === this.otherEvent.point[0];
}
};
module.exports = SweepEvent;
},{"./edge_type":8,"./signed_area":12}],14:[function(require,module,exports){
'use strict';
module.exports = TinyQueue;
module.exports.default = TinyQueue;
function TinyQueue(data, compare) {
if (!(this instanceof TinyQueue)) return new TinyQueue(data, compare);
this.data = data || [];
this.length = this.data.length;
this.compare = compare || defaultCompare;
if (this.length > 0) {
for (var i = (this.length >> 1) - 1; i >= 0; i--) this._down(i);
}
}
function defaultCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
TinyQueue.prototype = {
push: function (item) {
this.data.push(item);
this.length++;
this._up(this.length - 1);
},
pop: function () {
if (this.length === 0) return undefined;
var top = this.data[0];
this.length--;
if (this.length > 0) {
this.data[0] = this.data[this.length];
this._down(0);
}
this.data.pop();
return top;
},
peek: function () {
return this.data[0];
},
_up: function (pos) {
var data = this.data;
var compare = this.compare;
var item = data[pos];
while (pos > 0) {
var parent = (pos - 1) >> 1;
var current = data[parent];
if (compare(item, current) >= 0) break;
data[pos] = current;
pos = parent;
}
data[pos] = item;
},
_down: function (pos) {
var data = this.data;
var compare = this.compare;
var halfLength = this.length >> 1;
var item = data[pos];
while (pos < halfLength) {
var left = (pos << 1) + 1;
var right = left + 1;
var best = data[left];
if (right < this.length && compare(data[right], best) < 0) {
left = right;
best = data[right];
}
if (compare(best, item) >= 0) break;
data[pos] = best;
pos = left;
}
data[pos] = item;
}
};
},{}],15:[function(require,module,exports){
/**
* Offset edge of the polygon
*
* @param {Object} current
* @param {Object} next
* @constructor
*/
function Edge(current, next) {
/**
* @type {Object}
*/
this.current = current;
/**
* @type {Object}
*/
this.next = next;
/**
* @type {Object}
*/
this._inNormal = this.inwardsNormal();
/**
* @type {Object}
*/
this._outNormal = this.outwardsNormal();
}
/**
* Creates outwards normal
* @return {Object}
*/
Edge.prototype.outwardsNormal = function() {
var inwards = this.inwardsNormal();
return [
-inwards[0],
-inwards[1]
];
};
/**
* Creates inwards normal
* @return {Object}
*/
Edge.prototype.inwardsNormal = function() {
var dx = this.next[0] - this.current[0],
dy = this.next[1] - this.current[1],
edgeLength = Math.sqrt(dx * dx + dy * dy);
if (edgeLength === 0) throw new Error('Vertices overlap');
return [
-dy / edgeLength,
dx / edgeLength
];
};
/**
* Offsets the edge by dx, dy
* @param {Number} dx
* @param {Number} dy
* @return {Edge}
*/
Edge.prototype.offset = function(dx, dy) {
return Edge.offsetEdge(this.current, this.next, dx, dy);
};
/**
* @param {Number} dx
* @param {Number} dy
* @return {Edge}
*/
Edge.prototype.inverseOffset = function(dx, dy) {
return Edge.offsetEdge(this.next, this.current, dx, dy);
};
/**
* @static
* @param {Array.<Number>} current
* @param {Array.<Number>} next
* @param {Number} dx
* @param {Number} dy
* @return {Edge}
*/
Edge.offsetEdge = function(current, next, dx, dy) {
return new Edge([
current[0] + dx,
current[1] + dy
], [
next[0] + dx,
next[1] + dy
]);
};
/**
*
* @return {Edge}
*/
Edge.prototype.inverse = function () {
return new Edge(this.next, this.current);
};
module.exports = Edge;
},{}],16:[function(require,module,exports){
var Edge = require('./edge');
var martinez = require('martinez-polygon-clipping');
var utils = require('./utils');
var isArray = utils.isArray;
var equals = utils.equals;
var orientRings = utils.orientRings;
/**
* Offset builder
*
* @param {Array.<Object>=} vertices
* @param {Number=} arcSegments
* @constructor
*/
function Offset(vertices, arcSegments) {
/**
* @type {Array.<Object>}
*/
this.vertices = null;
/**
* @type {Array.<Edge>}
*/
this.edges = null;
/**
* @type {Boolean}
*/
this._closed = false;
/**
* @type {Number}
*/
this._distance = 0;
if (vertices) {
this.data(vertices);
}
/**
* Segments in edge bounding arches
* @type {Number}
*/
this._arcSegments = arcSegments !== undefined ? arcSegments : 5;
}
/**
* Change data set
* @param {Array.<Array>} vertices
* @return {Offset}
*/
Offset.prototype.data = function(vertices) {
this._edges = [];
if (!isArray (vertices)) {
throw new Error('Offset requires at least one coodinate to work with');
}
if (isArray(vertices) && typeof vertices[0] === 'number') {
this.vertices = vertices;
} else {
this.vertices = orientRings(vertices);
this._processContour(this.vertices, this._edges);
}
return this;
};
/**
* Recursively process contour to create normals
* @param {*} contour
* @param {Array} edges
*/
Offset.prototype._processContour = function(contour, edges) {
var i, len;
if (isArray(contour[0]) && typeof contour[0][0] === 'number') {
len = contour.length;
if (equals(contour[0], contour[len - 1])) {
len -= 1; // otherwise we get division by zero in normals
}
for (i = 0; i < len; i++) {
edges.push(new Edge(contour[i], contour[(i + 1) % len]));
}
} else {
for (i = 0, len = contour.length; i < len; i++) {
edges.push([]);
this._processContour(contour[i], edges[edges.length - 1]);
}
}
};
/**
* @param {Number} arcSegments
* @return {Offset}
*/
Offset.prototype.arcSegments = function(a