UNPKG

polygon-offset

Version:

Polygon offsetting algorithm, aimed for use with leaflet

1,876 lines (1,553 loc) 190 kB
(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: '&#x2206;' } }); L.NewLineControl = L.EditControl.extend({ options: { position: 'topleft', kind: 'polyline', html: '/' } }); L.NewPointControl = L.EditControl.extend({ options: { position: 'topleft', kind: 'point', html: '&#9679;' } }); },{}],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