UNPKG

polygon-offset

Version:

Polygon offsetting algorithm, aimed for use with leaflet

1,958 lines (1,598 loc) 59.1 kB
(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