mirror of
https://github.com/anvaka/isect.git
synced 2025-12-08 18:26:10 +00:00
Works very well - see performance results! Also fixes https://github.com/anvaka/isect/issues/1
2116 lines
53 KiB
JavaScript
2116 lines
53 KiB
JavaScript
/* follows "An implementation of top-down splaying"
|
|
* by D. Sleator <sleator@cs.cmu.edu> March 1992
|
|
*/
|
|
|
|
/**
|
|
* @typedef {*} Key
|
|
*/
|
|
|
|
|
|
/**
|
|
* @typedef {*} Value
|
|
*/
|
|
|
|
|
|
/**
|
|
* @typedef {function(node:Node):void} Visitor
|
|
*/
|
|
|
|
|
|
/**
|
|
* @typedef {function(a:Key, b:Key):number} Comparator
|
|
*/
|
|
|
|
|
|
/**
|
|
* @param {function(node:Node):string} NodePrinter
|
|
*/
|
|
|
|
|
|
/**
|
|
* @typedef {Object} Node
|
|
* @property {Key} Key
|
|
* @property {Value=} data
|
|
* @property {Node} left
|
|
* @property {Node} right
|
|
*/
|
|
|
|
var Node = function Node (key, data) {
|
|
this.key = key;
|
|
this.data = data;
|
|
this.left = null;
|
|
this.right= null;
|
|
};
|
|
|
|
function DEFAULT_COMPARE (a, b) { return a > b ? 1 : a < b ? -1 : 0; }
|
|
|
|
|
|
/**
|
|
* Simple top down splay, not requiring i to be in the tree t.
|
|
* @param {Key} i
|
|
* @param {Node?} t
|
|
* @param {Comparator} comparator
|
|
*/
|
|
function splay (i, t, comparator) {
|
|
if (t === null) { return t; }
|
|
var l, r, y;
|
|
var N = new Node();
|
|
l = r = N;
|
|
|
|
while (true) {
|
|
var cmp = comparator(i, t.key);
|
|
//if (i < t.key) {
|
|
if (cmp < 0) {
|
|
if (t.left === null) { break; }
|
|
//if (i < t.left.key) {
|
|
if (comparator(i, t.left.key) < 0) {
|
|
y = t.left; /* rotate right */
|
|
t.left = y.right;
|
|
y.right = t;
|
|
t = y;
|
|
if (t.left === null) { break; }
|
|
}
|
|
r.left = t; /* link right */
|
|
r = t;
|
|
t = t.left;
|
|
//} else if (i > t.key) {
|
|
} else if (cmp > 0) {
|
|
if (t.right === null) { break; }
|
|
//if (i > t.right.key) {
|
|
if (comparator(i, t.right.key) > 0) {
|
|
y = t.right; /* rotate left */
|
|
t.right = y.left;
|
|
y.left = t;
|
|
t = y;
|
|
if (t.right === null) { break; }
|
|
}
|
|
l.right = t; /* link left */
|
|
l = t;
|
|
t = t.right;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
/* assemble */
|
|
l.right = t.left;
|
|
r.left = t.right;
|
|
t.left = N.right;
|
|
t.right = N.left;
|
|
return t;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {Key} i
|
|
* @param {Value} data
|
|
* @param {Comparator} comparator
|
|
* @param {Tree} tree
|
|
* @return {Node} root
|
|
*/
|
|
function insert (i, data, t, comparator, tree) {
|
|
var node = new Node(i, data);
|
|
|
|
tree._size++;
|
|
|
|
if (t === null) {
|
|
node.left = node.right = null;
|
|
return node;
|
|
}
|
|
|
|
t = splay(i, t, comparator);
|
|
var cmp = comparator(i, t.key);
|
|
if (cmp < 0) {
|
|
node.left = t.left;
|
|
node.right = t;
|
|
t.left = null;
|
|
} else if (cmp >= 0) {
|
|
node.right = t.right;
|
|
node.left = t;
|
|
t.right = null;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
|
|
/**
|
|
* Insert i into the tree t, unless it's already there.
|
|
* @param {Key} i
|
|
* @param {Value} data
|
|
* @param {Comparator} comparator
|
|
* @param {Tree} tree
|
|
* @return {Node} root
|
|
*/
|
|
function add (i, data, t, comparator, tree) {
|
|
var node = new Node(i, data);
|
|
|
|
if (t === null) {
|
|
node.left = node.right = null;
|
|
tree._size++;
|
|
return node;
|
|
}
|
|
|
|
t = splay(i, t, comparator);
|
|
var cmp = comparator(i, t.key);
|
|
if (cmp === 0) { return t; }
|
|
else {
|
|
if (cmp < 0) {
|
|
node.left = t.left;
|
|
node.right = t;
|
|
t.left = null;
|
|
} else if (cmp > 0) {
|
|
node.right = t.right;
|
|
node.left = t;
|
|
t.right = null;
|
|
}
|
|
tree._size++;
|
|
return node;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes i from the tree if it's there
|
|
* @param {Key} i
|
|
* @param {Tree} tree
|
|
* @param {Comparator} comparator
|
|
* @param {Tree} tree
|
|
* @return {Node} new root
|
|
*/
|
|
function remove (i, t, comparator, tree) {
|
|
var x;
|
|
if (t === null) { return null; }
|
|
t = splay(i, t, comparator);
|
|
var cmp = comparator(i, t.key);
|
|
if (cmp === 0) { /* found it */
|
|
if (t.left === null) {
|
|
x = t.right;
|
|
} else {
|
|
x = splay(i, t.left, comparator);
|
|
x.right = t.right;
|
|
}
|
|
tree._size--;
|
|
return x;
|
|
}
|
|
return t; /* It wasn't there */
|
|
}
|
|
|
|
|
|
function split (key, v, comparator) {
|
|
var left, right;
|
|
if (v === null) {
|
|
left = right = null;
|
|
} else {
|
|
v = splay(key, v, comparator);
|
|
|
|
var cmp = comparator(v.key, key);
|
|
if (cmp === 0) {
|
|
left = v.left;
|
|
right = v.right;
|
|
} else if (cmp < 0) {
|
|
right = v.right;
|
|
v.right = null;
|
|
left = v;
|
|
} else {
|
|
left = v.left;
|
|
v.left = null;
|
|
right = v;
|
|
}
|
|
}
|
|
return { left: left, right: right };
|
|
}
|
|
|
|
|
|
function merge (left, right, comparator) {
|
|
if (right === null) { return left; }
|
|
if (left === null) { return right; }
|
|
|
|
right = splay(left.key, right, comparator);
|
|
right.left = left;
|
|
return right;
|
|
}
|
|
|
|
|
|
/**
|
|
* Prints level of the tree
|
|
* @param {Node} root
|
|
* @param {String} prefix
|
|
* @param {Boolean} isTail
|
|
* @param {Array<string>} out
|
|
* @param {Function(node:Node):String} printNode
|
|
*/
|
|
function printRow (root, prefix, isTail, out, printNode) {
|
|
if (root) {
|
|
out(("" + prefix + (isTail ? '└── ' : '├── ') + (printNode(root)) + "\n"));
|
|
var indent = prefix + (isTail ? ' ' : '│ ');
|
|
if (root.left) { printRow(root.left, indent, false, out, printNode); }
|
|
if (root.right) { printRow(root.right, indent, true, out, printNode); }
|
|
}
|
|
}
|
|
|
|
|
|
var Tree = function Tree (comparator) {
|
|
if ( comparator === void 0 ) comparator = DEFAULT_COMPARE;
|
|
|
|
this._comparator = comparator;
|
|
this._root = null;
|
|
this._size = 0;
|
|
};
|
|
|
|
var prototypeAccessors = { size: { configurable: true } };
|
|
|
|
|
|
/**
|
|
* Inserts a key, allows duplicates
|
|
* @param{Key} key
|
|
* @param{Value=} data
|
|
* @return {Node|null}
|
|
*/
|
|
Tree.prototype.insert = function insert$1 (key, data) {
|
|
return this._root = insert(key, data, this._root, this._comparator, this);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds a key, if it is not present in the tree
|
|
* @param{Key} key
|
|
* @param{Value=} data
|
|
* @return {Node|null}
|
|
*/
|
|
Tree.prototype.add = function add$1 (key, data) {
|
|
return this._root = add(key, data, this._root, this._comparator, this);
|
|
};
|
|
|
|
|
|
/**
|
|
* @param{Key} key
|
|
* @return {Node|null}
|
|
*/
|
|
Tree.prototype.remove = function remove$1 (key) {
|
|
this._root = remove(key, this._root, this._comparator, this);
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes and returns the node with smallest key
|
|
* @return {?Node}
|
|
*/
|
|
Tree.prototype.pop = function pop () {
|
|
var node = this._root;
|
|
if (node) {
|
|
while (node.left) { node = node.left; }
|
|
this._root = splay(node.key,this._root, this._comparator);
|
|
this._root = remove(node.key, this._root, this._comparator, this);
|
|
return { key: node.key, data: node.data };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param{Key} key
|
|
* @return {Node|null}
|
|
*/
|
|
Tree.prototype.findStatic = function findStatic (key) {
|
|
var current = this._root;
|
|
var compare = this._comparator;
|
|
while (current) {
|
|
var cmp = compare(key, current.key);
|
|
if (cmp === 0) { return current; }
|
|
else if (cmp < 0) { current = current.left; }
|
|
else { current = current.right; }
|
|
}
|
|
return null;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param{Key} key
|
|
* @return {Node|null}
|
|
*/
|
|
Tree.prototype.find = function find (key) {
|
|
if (this._root) {
|
|
this._root = splay(key, this._root, this._comparator);
|
|
if (this._comparator(key, this._root.key) !== 0) { return null; }
|
|
}
|
|
return this._root;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param{Key} key
|
|
* @return {Boolean}
|
|
*/
|
|
Tree.prototype.contains = function contains (key) {
|
|
var current = this._root;
|
|
var compare = this._comparator;
|
|
while (current) {
|
|
var cmp = compare(key, current.key);
|
|
if (cmp === 0) { return true; }
|
|
else if (cmp < 0) { current = current.left; }
|
|
else { current = current.right; }
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param{Visitor} visitor
|
|
* @param{*=} ctx
|
|
* @return {SplayTree}
|
|
*/
|
|
Tree.prototype.forEach = function forEach (visitor, ctx) {
|
|
var current = this._root;
|
|
var Q = [];/* Initialize stack s */
|
|
var done = false;
|
|
|
|
while (!done) {
|
|
if (current !==null) {
|
|
Q.push(current);
|
|
current = current.left;
|
|
} else {
|
|
if (Q.length !== 0) {
|
|
current = Q.pop();
|
|
visitor.call(ctx, current);
|
|
|
|
current = current.right;
|
|
} else { done = true; }
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Walk key range from `low` to `high`. Stops if `fn` returns a value.
|
|
* @param{Key} low
|
|
* @param{Key} high
|
|
* @param{Function} fn
|
|
* @param{*?} ctx
|
|
* @return {SplayTree}
|
|
*/
|
|
Tree.prototype.range = function range (low, high, fn, ctx) {
|
|
var this$1 = this;
|
|
|
|
var Q = [];
|
|
var compare = this._comparator;
|
|
var node = this._root, cmp;
|
|
|
|
while (Q.length !== 0 || node) {
|
|
if (node) {
|
|
Q.push(node);
|
|
node = node.left;
|
|
} else {
|
|
node = Q.pop();
|
|
cmp = compare(node.key, high);
|
|
if (cmp > 0) {
|
|
break;
|
|
} else if (compare(node.key, low) >= 0) {
|
|
if (fn.call(ctx, node)) { return this$1; } // stop if smth is returned
|
|
}
|
|
node = node.right;
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns array of keys
|
|
* @return {Array<Key>}
|
|
*/
|
|
Tree.prototype.keys = function keys () {
|
|
var keys = [];
|
|
this.forEach(function (ref) {
|
|
var key = ref.key;
|
|
|
|
return keys.push(key);
|
|
});
|
|
return keys;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns array of all the data in the nodes
|
|
* @return {Array<Value>}
|
|
*/
|
|
Tree.prototype.values = function values () {
|
|
var values = [];
|
|
this.forEach(function (ref) {
|
|
var data = ref.data;
|
|
|
|
return values.push(data);
|
|
});
|
|
return values;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {Key|null}
|
|
*/
|
|
Tree.prototype.min = function min () {
|
|
if (this._root) { return this.minNode(this._root).key; }
|
|
return null;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {Key|null}
|
|
*/
|
|
Tree.prototype.max = function max () {
|
|
if (this._root) { return this.maxNode(this._root).key; }
|
|
return null;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {Node|null}
|
|
*/
|
|
Tree.prototype.minNode = function minNode (t) {
|
|
if ( t === void 0 ) t = this._root;
|
|
|
|
if (t) { while (t.left) { t = t.left; } }
|
|
return t;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {Node|null}
|
|
*/
|
|
Tree.prototype.maxNode = function maxNode (t) {
|
|
if ( t === void 0 ) t = this._root;
|
|
|
|
if (t) { while (t.right) { t = t.right; } }
|
|
return t;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns node at given index
|
|
* @param{number} index
|
|
* @return {?Node}
|
|
*/
|
|
Tree.prototype.at = function at (index) {
|
|
var current = this._root, done = false, i = 0;
|
|
var Q = [];
|
|
|
|
while (!done) {
|
|
if (current) {
|
|
Q.push(current);
|
|
current = current.left;
|
|
} else {
|
|
if (Q.length > 0) {
|
|
current = Q.pop();
|
|
if (i === index) { return current; }
|
|
i++;
|
|
current = current.right;
|
|
} else { done = true; }
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param{Node} d
|
|
* @return {Node|null}
|
|
*/
|
|
Tree.prototype.next = function next (d) {
|
|
var root = this._root;
|
|
var successor = null;
|
|
|
|
if (d.right) {
|
|
successor = d.right;
|
|
while (successor.left) { successor = successor.left; }
|
|
return successor;
|
|
}
|
|
|
|
var comparator = this._comparator;
|
|
while (root) {
|
|
var cmp = comparator(d.key, root.key);
|
|
if (cmp === 0) { break; }
|
|
else if (cmp < 0) {
|
|
successor = root;
|
|
root = root.left;
|
|
} else { root = root.right; }
|
|
}
|
|
|
|
return successor;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param{Node} d
|
|
* @return {Node|null}
|
|
*/
|
|
Tree.prototype.prev = function prev (d) {
|
|
var root = this._root;
|
|
var predecessor = null;
|
|
|
|
if (d.left !== null) {
|
|
predecessor = d.left;
|
|
while (predecessor.right) { predecessor = predecessor.right; }
|
|
return predecessor;
|
|
}
|
|
|
|
var comparator = this._comparator;
|
|
while (root) {
|
|
var cmp = comparator(d.key, root.key);
|
|
if (cmp === 0) { break; }
|
|
else if (cmp < 0) { root = root.left; }
|
|
else {
|
|
predecessor = root;
|
|
root = root.right;
|
|
}
|
|
}
|
|
return predecessor;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {SplayTree}
|
|
*/
|
|
Tree.prototype.clear = function clear () {
|
|
this._root = null;
|
|
this._size = 0;
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {NodeList}
|
|
*/
|
|
Tree.prototype.toList = function toList$1 () {
|
|
return toList(this._root);
|
|
};
|
|
|
|
|
|
/**
|
|
* Bulk-load items. Both array have to be same size
|
|
* @param{Array<Key>} keys
|
|
* @param{Array<Value>}[values]
|
|
* @param{Boolean} [presort=false] Pre-sort keys and values, using
|
|
* tree's comparator. Sorting is done
|
|
* in-place
|
|
* @return {AVLTree}
|
|
*/
|
|
Tree.prototype.load = function load (keys, values, presort) {
|
|
if ( keys === void 0 ) keys = [];
|
|
if ( values === void 0 ) values = [];
|
|
if ( presort === void 0 ) presort = false;
|
|
|
|
var size = keys.length;
|
|
var comparator = this._comparator;
|
|
|
|
// sort if needed
|
|
if (presort) { sort(keys, values, 0, size - 1, comparator); }
|
|
|
|
if (this._root === null) { // empty tree
|
|
this._root = loadRecursive(this._root, keys, values, 0, size);
|
|
this._size = size;
|
|
} else { // that re-builds the whole tree from two in-order traversals
|
|
var mergedList = mergeLists(this.toList(), createList(keys, values), comparator);
|
|
size = this._size + size;
|
|
this._root = sortedListToBST({ head: mergedList }, 0, size);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {Boolean}
|
|
*/
|
|
Tree.prototype.isEmpty = function isEmpty () { return this._root === null; };
|
|
|
|
prototypeAccessors.size.get = function () { return this._size; };
|
|
|
|
|
|
/**
|
|
* @param{NodePrinter=} printNode
|
|
* @return {String}
|
|
*/
|
|
Tree.prototype.toString = function toString (printNode) {
|
|
if ( printNode === void 0 ) printNode = function (n) { return n.key; };
|
|
|
|
var out = [];
|
|
printRow(this._root, '', true, function (v) { return out.push(v); }, printNode);
|
|
return out.join('');
|
|
};
|
|
|
|
|
|
Tree.prototype.update = function update (key, newKey, newData) {
|
|
var comparator = this._comparator;
|
|
var ref = split(key, this._root, comparator);
|
|
var left = ref.left;
|
|
var right = ref.right;
|
|
this._size--;
|
|
if (comparator(key, newKey) < 0) {
|
|
right = insert(newKey, newData, right, comparator, this);
|
|
} else {
|
|
left = insert(newKey, newData, left, comparator, this);
|
|
}
|
|
this._root = merge(left, right, comparator);
|
|
};
|
|
|
|
|
|
Tree.prototype.split = function split$1 (key) {
|
|
return split(key, this._root, this._comparator);
|
|
};
|
|
|
|
Object.defineProperties( Tree.prototype, prototypeAccessors );
|
|
|
|
|
|
function loadRecursive (parent, keys, values, start, end) {
|
|
var size = end - start;
|
|
if (size > 0) {
|
|
var middle = start + Math.floor(size / 2);
|
|
var key = keys[middle];
|
|
var data = values[middle];
|
|
var node = { key: key, data: data, parent: parent };
|
|
node.left = loadRecursive(node, keys, values, start, middle);
|
|
node.right = loadRecursive(node, keys, values, middle + 1, end);
|
|
return node;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
function createList(keys, values) {
|
|
var head = { next: null };
|
|
var p = head;
|
|
for (var i = 0; i < keys.length; i++) {
|
|
p = p.next = { key: keys[i], data: values[i] };
|
|
}
|
|
p.next = null;
|
|
return head.next;
|
|
}
|
|
|
|
|
|
function toList (root) {
|
|
var current = root;
|
|
var Q = [], done = false;
|
|
|
|
var head = { next: null };
|
|
var p = head;
|
|
|
|
while (!done) {
|
|
if (current) {
|
|
Q.push(current);
|
|
current = current.left;
|
|
} else {
|
|
if (Q.length > 0) {
|
|
current = p = p.next = Q.pop();
|
|
current = current.right;
|
|
} else { done = true; }
|
|
}
|
|
}
|
|
p.next = null; // that'll work even if the tree was empty
|
|
return head.next;
|
|
}
|
|
|
|
|
|
function sortedListToBST(list, start, end) {
|
|
var size = end - start;
|
|
if (size > 0) {
|
|
var middle = start + Math.floor(size / 2);
|
|
var left = sortedListToBST(list, start, middle);
|
|
|
|
var root = list.head;
|
|
root.left = left;
|
|
|
|
list.head = list.head.next;
|
|
|
|
root.right = sortedListToBST(list, middle + 1, end);
|
|
return root;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
function mergeLists (l1, l2, compare) {
|
|
if ( compare === void 0 ) compare = function (a, b) { return a - b; };
|
|
|
|
var head = {}; // dummy
|
|
var p = head;
|
|
|
|
var p1 = l1;
|
|
var p2 = l2;
|
|
|
|
while (p1 !== null && p2 !== null) {
|
|
if (compare(p1.key, p2.key) < 0) {
|
|
p.next = p1;
|
|
p1 = p1.next;
|
|
} else {
|
|
p.next = p2;
|
|
p2 = p2.next;
|
|
}
|
|
p = p.next;
|
|
}
|
|
|
|
if (p1 !== null) { p.next = p1; }
|
|
else if (p2 !== null) { p.next = p2; }
|
|
|
|
return head.next;
|
|
}
|
|
|
|
|
|
function sort(keys, values, left, right, compare) {
|
|
if (left >= right) { return; }
|
|
|
|
var pivot = keys[(left + right) >> 1];
|
|
var i = left - 1;
|
|
var j = right + 1;
|
|
|
|
while (true) {
|
|
do { i++; } while (compare(keys[i], pivot) < 0);
|
|
do { j--; } while (compare(keys[j], pivot) > 0);
|
|
if (i >= j) { break; }
|
|
|
|
var tmp = keys[i];
|
|
keys[i] = keys[j];
|
|
keys[j] = tmp;
|
|
|
|
tmp = values[i];
|
|
values[i] = values[j];
|
|
values[j] = tmp;
|
|
}
|
|
|
|
sort(keys, values, left, j, compare);
|
|
sort(keys, values, j + 1, right, compare);
|
|
}
|
|
|
|
function createEventQueue(byY) {
|
|
var q = new Tree(byY);
|
|
|
|
return {
|
|
isEmpty: isEmpty,
|
|
size: size,
|
|
pop: pop,
|
|
find: find,
|
|
insert: insert
|
|
}
|
|
|
|
function find(p) {
|
|
return q.find(p);
|
|
}
|
|
|
|
function size() {
|
|
return q.size;
|
|
}
|
|
|
|
function isEmpty() {
|
|
return q.isEmpty();
|
|
}
|
|
|
|
function insert(event) {
|
|
// debugger;
|
|
q.add(event.point, event);
|
|
}
|
|
|
|
function pop() {
|
|
var node = q.pop();
|
|
return node && node.data;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Just a collection of geometry related utilities
|
|
*/
|
|
|
|
// This is used for precision checking (e.g. two numbers are equal
|
|
// if their difference is smaller than this number). The value is
|
|
// chosen empirically. We still may run into precision related issues.
|
|
// TODO: we should allow consumers to configure this.
|
|
var EPS = 1e-9;//10;
|
|
|
|
function getIntersectionXPoint(segment, xPos, yPos) {
|
|
var dy1 = segment.from.y - yPos;
|
|
var dy2 = yPos - segment.to.y;
|
|
var dy = segment.to.y - segment.from.y;
|
|
if (Math.abs(dy1) < EPS) {
|
|
// The segment starts on the sweep line
|
|
if (Math.abs(dy) < EPS) {
|
|
// the segment is horizontal. Intersection is at the point
|
|
if (xPos <= segment.from.x) { return segment.from.x; }
|
|
if (xPos > segment.to.x) { return segment.to.x; }
|
|
return xPos;
|
|
}
|
|
return segment.from.x;
|
|
}
|
|
|
|
var dx = (segment.to.x - segment.from.x);
|
|
var xOffset;
|
|
if (dy1 >= dy2) {
|
|
xOffset = dy1 * (dx / dy);
|
|
return (segment.from.x - xOffset);
|
|
}
|
|
xOffset = dy2 * (dx / dy);
|
|
return (segment.to.x + xOffset);
|
|
}
|
|
|
|
function angle(dx, dy) {
|
|
// https://stackoverflow.com/questions/16542042/fastest-way-to-sort-vectors-by-angle-without-actually-computing-that-angle
|
|
var p = dx/(Math.abs(dx) + Math.abs(dy)); // -1 .. 1 increasing with x
|
|
|
|
if (dy < 0) { return p - 1; } // -2 .. 0 increasing with x
|
|
return 1 - p // 0 .. 2 decreasing with x
|
|
}
|
|
|
|
function intersectSegments(a, b) {
|
|
// https://stackoverflow.com/a/1968345/125351
|
|
var aStart = a.from, bStart = b.from;
|
|
var p0_x = aStart.x, p0_y = aStart.y,
|
|
p2_x = bStart.x, p2_y = bStart.y;
|
|
|
|
var s1_x = a.dx, s1_y = a.dy, s2_x = b.dx, s2_y = b.dy;
|
|
var div = s1_x * s2_y - s2_x * s1_y;
|
|
|
|
var s = (s1_y * (p0_x - p2_x) - s1_x * (p0_y - p2_y)) / div;
|
|
if (s < 0 || s > 1) { return; }
|
|
|
|
var t = (s2_x * (p2_y - p0_y) + s2_y * (p0_x - p2_x)) / div;
|
|
|
|
if (t >= 0 && t <= 1) {
|
|
return {
|
|
x: p0_x - (t * s1_x),
|
|
y: p0_y - (t * s1_y)
|
|
}
|
|
}
|
|
}
|
|
|
|
function samePoint(a, b) {
|
|
return Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS;
|
|
}
|
|
|
|
/**
|
|
* Creates a new sweep status data structure.
|
|
*/
|
|
function createSweepStatus(onError, EPS$$1) {
|
|
var lastPointY, prevY;
|
|
var lastPointX, prevX;
|
|
var useBelow = false;
|
|
var status = new Tree(compareSegments);
|
|
|
|
// To save on GC we return mutable object.
|
|
var currentBoundary = {
|
|
beforeLeft: null,
|
|
left: null,
|
|
right: null,
|
|
afterRight: null,
|
|
};
|
|
|
|
var currentLeftRight = {left: null, right: null};
|
|
|
|
return {
|
|
/**
|
|
* Add new segments into the status tree.
|
|
*/
|
|
insertSegments: insertSegments,
|
|
|
|
/**
|
|
* Remove segments from the status tree.
|
|
*/
|
|
deleteSegments: deleteSegments,
|
|
|
|
/**
|
|
* Returns segments that are to the left and right from a given point.
|
|
*/
|
|
getLeftRightPoint: getLeftRightPoint,
|
|
|
|
/**
|
|
* For a given collections of segments finds the most left and the most right
|
|
* segments. Also returns segments immediately before left, and after right segments.
|
|
*/
|
|
getBoundarySegments: getBoundarySegments,
|
|
|
|
findSegmentsWithPoint: findSegmentsWithPoint,
|
|
|
|
/**
|
|
* Current binary search tree with segments
|
|
*/
|
|
status: status,
|
|
|
|
/**
|
|
* Introspection method that verifies if there are duplicates in the segment tree.
|
|
* If there are - `onError()` is called.
|
|
*/
|
|
checkDuplicate: checkDuplicate,
|
|
|
|
/**
|
|
* Prints current segments in order of their intersection with sweep line. Introspection method.
|
|
*/
|
|
printStatus: printStatus,
|
|
|
|
/**
|
|
* Returns current position of the sweep line.
|
|
*/
|
|
getLastPoint: function getLastPoint() {
|
|
return {x: lastPointX, y: lastPointY};
|
|
}
|
|
}
|
|
|
|
function compareSegments(a, b) {
|
|
if (a === b) { return 0; }
|
|
|
|
var ak = getIntersectionXPoint(a, lastPointX, lastPointY);
|
|
var bk = getIntersectionXPoint(b, lastPointX, lastPointY);
|
|
|
|
var res = ak - bk;
|
|
if (Math.abs(res) >= EPS$$1) {
|
|
// We are okay fine. Intersection distance between two segments
|
|
// is good to give conclusive answer
|
|
return res;
|
|
}
|
|
|
|
var aIsHorizontal = Math.abs(a.dy) < EPS$$1;
|
|
var bIsHorizontal = Math.abs(b.dy) < EPS$$1;
|
|
if (aIsHorizontal && bIsHorizontal) {
|
|
return b.to.x - a.to.x;
|
|
}
|
|
// TODO: What if both a and b is horizontal?
|
|
// move horizontal to end
|
|
if (aIsHorizontal) {
|
|
return useBelow ? -1 : 1;
|
|
}
|
|
|
|
if (bIsHorizontal) {
|
|
if (useBelow) {
|
|
return (b.from.x >= lastPointX) ? -1 : 1
|
|
}
|
|
return -1;
|
|
// return useBelow ? 1 : -1;
|
|
}
|
|
var pa = a.angle;
|
|
var pb = b.angle;
|
|
if (Math.abs(pa - pb) >= EPS$$1) {
|
|
return useBelow ? pa - pb : pb - pa;
|
|
}
|
|
|
|
var segDist = a.from.y - b.from.y;
|
|
if (Math.abs(segDist) >= EPS$$1) {
|
|
return -segDist;
|
|
}
|
|
segDist = a.to.y - b.to.y;
|
|
if (Math.abs(segDist) >= EPS$$1) {
|
|
// TODO: Is this accurate?
|
|
return -segDist;
|
|
}
|
|
|
|
return 0;
|
|
// Could also use:
|
|
// var aAngle = Math.atan2(a.from.y - a.to.y, a.from.x - a.to.x);
|
|
// var bAngle = Math.atan2(b.from.y - b.to.y, b.from.x - b.to.x);
|
|
// return useBelow ? bAngle - aAngle : aAngle - bAngle;
|
|
}
|
|
|
|
function getBoundarySegments(upper, interior) {
|
|
var leftMost, rightMost, i;
|
|
var uLength = upper.length;
|
|
|
|
if (uLength > 0) {
|
|
leftMost = rightMost = upper[0];
|
|
} else {
|
|
leftMost = rightMost = interior[0];
|
|
}
|
|
|
|
for (i = 1; i < uLength; ++i) {
|
|
var s = upper[i];
|
|
var cmp = compareSegments(leftMost, s);
|
|
if (cmp > 0) { leftMost = s; }
|
|
|
|
cmp = compareSegments(rightMost, s);
|
|
if (cmp < 0) { rightMost = s; }
|
|
}
|
|
|
|
var startFrom = uLength > 0 ? 0 : 1;
|
|
for (i = startFrom; i < interior.length; ++i) {
|
|
s = interior[i];
|
|
cmp = compareSegments(leftMost, s);
|
|
if (cmp > 0) { leftMost = s; }
|
|
|
|
cmp = compareSegments(rightMost, s);
|
|
if (cmp < 0) { rightMost = s; }
|
|
}
|
|
|
|
// at this point we have our left/right segments in the status.
|
|
// Let's find their prev/next elements and report them back:
|
|
var left = status.find(leftMost);
|
|
if (!left) {
|
|
onError('Left is missing. Precision error?');
|
|
}
|
|
|
|
var right = status.find(rightMost);
|
|
if (!right) {
|
|
onError('Right is missing. Precision error?');
|
|
}
|
|
|
|
var beforeLeft = left && status.prev(left);
|
|
var afterRight = right && status.next(right);
|
|
|
|
while (afterRight && right.key.dy === 0 && afterRight.key.dy === 0) {
|
|
// horizontal segments are special :(
|
|
afterRight = status.next(afterRight);
|
|
}
|
|
|
|
currentBoundary.beforeLeft = beforeLeft && beforeLeft.key;
|
|
currentBoundary.left = left && left.key;
|
|
currentBoundary.right = right && right.key;
|
|
currentBoundary.afterRight = afterRight && afterRight.key;
|
|
|
|
return currentBoundary;
|
|
}
|
|
|
|
function getLeftRightPoint(p) {
|
|
// We are trying to find left and right segments that are nearest to the
|
|
// point p. For this we traverse the binary search tree, and remember
|
|
// node with the shortest distance to p.
|
|
var lastLeft;
|
|
var current = status._root;
|
|
var minX = Number.POSITIVE_INFINITY;
|
|
while (current) {
|
|
var x = getIntersectionXPoint(current.key, p.x, p.y);
|
|
var dx = p.x - x;
|
|
if (dx >= 0) {
|
|
if (dx < minX) {
|
|
minX = dx;
|
|
lastLeft = current;
|
|
current = current.left;
|
|
} else {
|
|
break;
|
|
}
|
|
} else {
|
|
if (-dx < minX) {
|
|
minX = -dx;
|
|
lastLeft = current;
|
|
current = current.right;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
currentLeftRight.left = lastLeft && lastLeft.key;
|
|
var next = lastLeft && status.next(lastLeft);
|
|
currentLeftRight.right = next && next.key;
|
|
return currentLeftRight;
|
|
|
|
// Conceptually, the code above should be equivalent to the code below;
|
|
// The code below is easier to understand, but intuitively, the code above
|
|
// should have better performance (as we do not traverse the entire status
|
|
// tree)
|
|
|
|
// var right, left, x;
|
|
// var all = status.keys()
|
|
// for (var i = 0; i < all.length; ++i) {
|
|
// var segment = all[i];
|
|
// x = getIntersectionXPoint(segment, p.x, p.y);
|
|
// if (x > p.x && !right) {
|
|
// right = segment;
|
|
// break;
|
|
// } else if (x < p.x) {
|
|
// left = segment;
|
|
// }
|
|
// }
|
|
|
|
// currentLeftRight.left = left;
|
|
// currentLeftRight.right = right;
|
|
|
|
// return currentLeftRight;
|
|
}
|
|
|
|
function findSegmentsWithPoint(p, onFound) {
|
|
// Option 1.
|
|
// var arrResults = [];
|
|
// status.forEach(current => {
|
|
// var x = getIntersectionXPoint(current.key, p.x, p.y);
|
|
// var dx = p.x - x;
|
|
// if (Math.abs(dx) < EPS) {
|
|
// onFound(current.key);
|
|
// // arrResults.push(current.key)
|
|
// }
|
|
// });
|
|
// return arrResults;
|
|
|
|
// Option 2.
|
|
|
|
// let current = status._root;
|
|
// const Q = []; /* Initialize stack s */
|
|
// let done = false;
|
|
// var res = [];
|
|
// var breakEarly = false;
|
|
|
|
// while (!done) {
|
|
// if (current !== null) {
|
|
// Q.push(current);
|
|
// current = current.left;
|
|
// } else {
|
|
// if (Q.length !== 0) {
|
|
// current = Q.pop();
|
|
|
|
// var x = getIntersectionXPoint(current.key, p.x, p.y);
|
|
// var dx = p.x - x;
|
|
// if (Math.abs(dx) < EPS) {
|
|
// res.push(current.key)
|
|
// breakEarly = true;
|
|
// } else if (breakEarly) {
|
|
// done = true;
|
|
// }
|
|
|
|
// current = current.right;
|
|
// } else done = true;
|
|
// }
|
|
// }
|
|
|
|
// return res;
|
|
|
|
// option 3.
|
|
var current = status._root;
|
|
|
|
while (current) {
|
|
var x = getIntersectionXPoint(current.key, p.x, p.y);
|
|
var dx = p.x - x;
|
|
if (Math.abs(dx) < EPS$$1) {
|
|
collectAdjacentNodes(current, p, onFound);
|
|
break;
|
|
} else if (dx < 0) {
|
|
current = current.left;
|
|
} else {
|
|
current = current.right;
|
|
}
|
|
}
|
|
}
|
|
|
|
function collectAdjacentNodes(root, p, onFound) {
|
|
onFound(root.key);
|
|
goOverPredecessors(root.left, p, onFound);
|
|
goOverSuccessors(root.right, p, onFound);
|
|
}
|
|
|
|
function goOverPredecessors(root, p, res) {
|
|
if (!root) { return; }
|
|
var x = getIntersectionXPoint(root.key, p.x, p.y);
|
|
var dx = p.x - x;
|
|
if (Math.abs(dx) < EPS$$1) {
|
|
collectAdjacentNodes(root, p, res);
|
|
} else {
|
|
goOverPredecessors(root.right, p, res);
|
|
}
|
|
}
|
|
|
|
function goOverSuccessors(root, p, res) {
|
|
if (!root) { return; }
|
|
var x = getIntersectionXPoint(root.key, p.x, p.y);
|
|
var dx = p.x - x;
|
|
if (Math.abs(dx) < EPS$$1) {
|
|
collectAdjacentNodes(root, p, res);
|
|
} else {
|
|
goOverSuccessors(root.left, p, res);
|
|
}
|
|
}
|
|
|
|
function checkDuplicate() {
|
|
var prev;
|
|
status.forEach(function (node) {
|
|
var current = node.key;
|
|
|
|
if (prev) {
|
|
if (samePoint(prev.from, current.from) && samePoint(prev.to, current.to)) {
|
|
// Likely you have received error before during segment removal.
|
|
onError('Duplicate key in the status! This may be caused by Floating Point rounding error');
|
|
}
|
|
}
|
|
prev = current;
|
|
});
|
|
}
|
|
|
|
function printStatus(prefix) {
|
|
if ( prefix === void 0 ) prefix = '';
|
|
|
|
// eslint-disable-next-line
|
|
console.log(prefix, 'status line: ', lastPointX, lastPointY);
|
|
status.forEach(function (node) {
|
|
var x = getIntersectionXPoint(node.key, lastPointX, lastPointY);
|
|
// eslint-disable-next-line
|
|
console.log(x + ' ' + node.key.name);
|
|
});
|
|
}
|
|
|
|
function insertSegments(interior, upper, sweepLinePos) {
|
|
lastPointY = sweepLinePos.y;
|
|
lastPointX = sweepLinePos.x;
|
|
var key;
|
|
|
|
for (var i = 0; i < interior.length; ++i) {
|
|
key = interior[i];
|
|
status.add(key);
|
|
}
|
|
for (i = 0; i < upper.length; ++i) {
|
|
key = upper[i];
|
|
status.add(key);
|
|
}
|
|
}
|
|
|
|
function deleteSegments(lower, interior, sweepLinePos) {
|
|
// I spent most of the time debugging this method. Depending on the
|
|
// algorithm state we can run into situation when dynamic keys of the
|
|
// `status` tree predict wrong branch, and thus we are not able to find
|
|
// the segment that needs to be deleted. If that happens I'm trying to
|
|
// use previous point and repeat the process. This may result in
|
|
// incorrect state. In that case I report an error.
|
|
var i;
|
|
var prevCount = status._size;
|
|
prevX = lastPointX;
|
|
prevY = lastPointY;
|
|
lastPointY = sweepLinePos.y;
|
|
lastPointX = sweepLinePos.x;
|
|
|
|
useBelow = true;
|
|
for(i = 0; i < lower.length; ++i) {
|
|
removeSegment(lower[i], sweepLinePos);
|
|
}
|
|
for(i = 0; i < interior.length; ++i) {
|
|
removeSegment(interior[i], sweepLinePos);
|
|
}
|
|
useBelow = false;
|
|
|
|
if (status._size !== prevCount - interior.length - lower.length) {
|
|
// This can happen when rounding error occurs. You can try scaling your input
|
|
onError('Segments were not removed from a tree properly. Precision error?');
|
|
}
|
|
}
|
|
|
|
function removeSegment(key, sweepLinePos) {
|
|
if (status.find(key)) {
|
|
status.remove(key);
|
|
} else {
|
|
lastPointX = prevX;
|
|
lastPointY = prevY;
|
|
if (status.find(key)) {
|
|
status.remove(key);
|
|
}
|
|
lastPointY = sweepLinePos.y;
|
|
lastPointX = sweepLinePos.x;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a single event in the sweep-line algorithm
|
|
*/
|
|
var SweepEvent = function SweepEvent(point, segment) {
|
|
this.point = point;
|
|
if (segment) { this.from = [segment]; }
|
|
};
|
|
|
|
/**
|
|
* A point on a line
|
|
*
|
|
* @typedef {Object} Point
|
|
* @property {number} x coordinate
|
|
* @property {number} y coordinate
|
|
*/
|
|
|
|
|
|
/**
|
|
* @typedef {Object} Segment
|
|
* @property {Point} from start of the segment
|
|
* @property {Point} to end of the segment
|
|
*/
|
|
|
|
/**
|
|
* @typedef {function(point : Point, interior : Segment[], lower : Segment[], upper : Segment[])} ReportIntersectionCallback
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} ISectOptions
|
|
* @property {ReportIntersectionCallback} onFound
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} ISectResult
|
|
*/
|
|
|
|
// We use EMPTY array to avoid pressure on garbage collector. Need to be
|
|
// very cautious to not mutate this array.
|
|
var EMPTY = [];
|
|
|
|
/**
|
|
* Finds all intersections among given segments.
|
|
*
|
|
* The algorithm follows "Computation Geometry, Algorithms and Applications" book
|
|
* by Mark de Berg, Otfried Cheong, Marc van Kreveld, and Mark Overmars.
|
|
*
|
|
* Line is swept top-down
|
|
*
|
|
* @param {Segment[]} segments
|
|
* @param {ISectOptions=} options
|
|
* @returns {ISectResult}
|
|
*/
|
|
function isect(segments, options) {
|
|
var results = [];
|
|
var reportIntersection = (options && options.onFound) || defaultIntersectionReporter;
|
|
|
|
var onError = (options && options.onError) || defaultErrorReporter;
|
|
|
|
var eventQueue = createEventQueue(byY);
|
|
var sweepStatus = createSweepStatus(onError, EPS);
|
|
var lower, interior, lastPoint;
|
|
|
|
segments.forEach(addSegment);
|
|
|
|
return {
|
|
/**
|
|
* Find all intersections synchronously.
|
|
*
|
|
* @returns array of found intersections.
|
|
*/
|
|
run: run,
|
|
|
|
/**
|
|
* Performs a single step in the sweep line algorithm
|
|
*
|
|
* @returns true if there was something to process; False if no more work to do
|
|
*/
|
|
step: step,
|
|
|
|
// Methods below are low level API for fine-grained control.
|
|
// Don't use it unless you understand this code thoroughly
|
|
|
|
/**
|
|
* Add segment into the
|
|
*/
|
|
addSegment: addSegment,
|
|
|
|
/**
|
|
* Direct access to event queue. Queue contains segment endpoints and
|
|
* pending detected intersections.
|
|
*/
|
|
eventQueue: eventQueue,
|
|
|
|
/**
|
|
* Direct access to sweep line status. "Status" holds information about
|
|
* all intersected segments.
|
|
*/
|
|
sweepStatus: sweepStatus,
|
|
|
|
/**
|
|
* Access to results array. Works only when you use default onFound() handler
|
|
*/
|
|
results: results
|
|
}
|
|
|
|
function run() {
|
|
while (!eventQueue.isEmpty()) {
|
|
var eventPoint = eventQueue.pop();
|
|
if (handleEventPoint(eventPoint)) {
|
|
// they decided to stop.
|
|
return;
|
|
} }
|
|
|
|
return results;
|
|
}
|
|
|
|
function step() {
|
|
if (!eventQueue.isEmpty()) {
|
|
var eventPoint = eventQueue.pop();
|
|
handleEventPoint(eventPoint);
|
|
// Note: we don't check results of `handleEventPoint()`
|
|
// assumption is that client controls `step()` and thus they
|
|
// know better if they want to stop.
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function handleEventPoint(p) {
|
|
lastPoint = p.point;
|
|
var upper = p.from || EMPTY;
|
|
|
|
lower = interior = undefined;
|
|
// TODO: move lower/interior into sweep status method?
|
|
|
|
sweepStatus.findSegmentsWithPoint(lastPoint, addLowerOrInterior);
|
|
// if (segmentsWithPoint) {
|
|
// segmentsWithPoint.forEach()
|
|
// }
|
|
|
|
if (!lower) { lower = EMPTY; }
|
|
if (!interior) { interior = EMPTY; }
|
|
|
|
var uLength = upper.length;
|
|
var iLength = interior.length;
|
|
var lLength = lower.length;
|
|
var hasIntersection = uLength + iLength + lLength > 1;
|
|
var hasPointIntersection = !hasIntersection && (uLength === 0 && lLength === 0 && iLength > 0);
|
|
|
|
if (hasIntersection || hasPointIntersection) {
|
|
p.isReported = true;
|
|
if (reportIntersection(lastPoint, union(interior, union(lower, upper)))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
sweepStatus.deleteSegments(lower, interior, lastPoint);
|
|
sweepStatus.insertSegments(interior, upper, lastPoint);
|
|
|
|
var sLeft, sRight;
|
|
|
|
var hasNoCrossing = (uLength + iLength === 0);
|
|
|
|
if (hasNoCrossing) {
|
|
var leftRight = sweepStatus.getLeftRightPoint(lastPoint);
|
|
sLeft = leftRight.left;
|
|
if (!sLeft) { return; }
|
|
|
|
sRight = leftRight.right;
|
|
if (!sRight) { return; }
|
|
|
|
findNewEvent(sLeft, sRight, p);
|
|
} else {
|
|
var boundarySegments = sweepStatus.getBoundarySegments(upper, interior);
|
|
|
|
findNewEvent(boundarySegments.beforeLeft, boundarySegments.left, p);
|
|
findNewEvent(boundarySegments.right, boundarySegments.afterRight, p);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function addLowerOrInterior(s) {
|
|
if (samePoint(s.to, lastPoint)) {
|
|
if (!lower) { lower = [s]; }
|
|
else { lower.push(s); }
|
|
} else if (!samePoint(s.from, lastPoint)) {
|
|
if (!interior) { interior = [s]; }
|
|
else { interior.push(s); }
|
|
}
|
|
}
|
|
|
|
function findNewEvent(left, right, p) {
|
|
if (!left || !right) { return; }
|
|
|
|
var intersection = intersectSegments(left, right);
|
|
if (!intersection) {
|
|
return;
|
|
}
|
|
|
|
var dy = p.point.y - intersection.y;
|
|
// TODO: should I add dy to intersection.y?
|
|
if (dy < -EPS) {
|
|
// this means intersection happened after the sweep line.
|
|
// We already processed it.
|
|
return;
|
|
}
|
|
if (Math.abs(dy) < EPS && intersection.x <= p.point.x) {
|
|
return;
|
|
}
|
|
|
|
// Need to adjust floating point for this special case,
|
|
// since otherwise it gives rounding errors:
|
|
roundNearZero(intersection);
|
|
|
|
var current = eventQueue.find(intersection);
|
|
|
|
if (current && current.isReported) {
|
|
// We already reported this event. No need to add it one more time
|
|
// TODO: Is this case even possible?
|
|
onError('We already reported this event.');
|
|
return;
|
|
}
|
|
|
|
if (!current) {
|
|
var event = new SweepEvent(intersection);
|
|
eventQueue.insert(event);
|
|
}
|
|
}
|
|
|
|
function defaultIntersectionReporter(p, segments) {
|
|
results.push({
|
|
point: p,
|
|
segments: segments
|
|
});
|
|
}
|
|
|
|
function addSegment(segment) {
|
|
var from = segment.from;
|
|
var to = segment.to;
|
|
|
|
// Small numbers give more precision errors. Rounding them to 0.
|
|
roundNearZero(from);
|
|
roundNearZero(to);
|
|
|
|
var dy = from.y - to.y;
|
|
|
|
// Note: dy is much smaller then EPS on purpose. I found that higher
|
|
// precision here does less good - getting way more rounding errors.
|
|
if (Math.abs(dy) < 1e-5) {
|
|
from.y = to.y;
|
|
segment.dy = 0;
|
|
}
|
|
if ((from.y < to.y) || (
|
|
(from.y === to.y) && (from.x > to.x))
|
|
) {
|
|
var temp = from;
|
|
from = segment.from = to;
|
|
to = segment.to = temp;
|
|
}
|
|
|
|
// We pre-compute some immutable properties of the segment
|
|
// They are used quite often in the tree traversal, and pre-computation
|
|
// gives significant boost:
|
|
segment.dy = from.y - to.y;
|
|
segment.dx = from.x - to.x;
|
|
segment.angle = angle(segment.dy, segment.dx);
|
|
|
|
var isPoint = segment.dy === segment.dx && segment.dy === 0;
|
|
var prev = eventQueue.find(from);
|
|
if (prev && !isPoint) {
|
|
// this detects identical segments early. Without this check
|
|
// the algorithm would break since sweep line has no means to
|
|
// detect identical segments.
|
|
var prevFrom = prev.data.from;
|
|
if (prevFrom) {
|
|
for (var i = 0; i < prevFrom.length; ++i) {
|
|
var s = prevFrom[i];
|
|
if (samePoint(s.to, to)) {
|
|
reportIntersection(s.from, [s.from, s.to]);
|
|
reportIntersection(s.to, [s.from, s.to]);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isPoint) {
|
|
if (prev) {
|
|
if (prev.data.from) { prev.data.from.push(segment); }
|
|
else { prev.data.from = [segment]; }
|
|
} else {
|
|
var e = new SweepEvent(from, segment);
|
|
eventQueue.insert(e);
|
|
}
|
|
var event = new SweepEvent(to);
|
|
eventQueue.insert(event);
|
|
} else {
|
|
var event = new SweepEvent(to);
|
|
eventQueue.insert(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
function roundNearZero(point) {
|
|
if (Math.abs(point.x) < EPS) { point.x = 0; }
|
|
if (Math.abs(point.y) < EPS) { point.y = 0; }
|
|
}
|
|
|
|
function defaultErrorReporter(errorMessage) {
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
function union(a, b) {
|
|
if (!a) { return b; }
|
|
if (!b) { return a; }
|
|
|
|
return a.concat(b);
|
|
}
|
|
|
|
function byY(a, b) {
|
|
// decreasing Y
|
|
var res = b.y - a.y;
|
|
// TODO: This might mess up the status tree.
|
|
if (Math.abs(res) < EPS) {
|
|
// increasing x.
|
|
res = a.x - b.x;
|
|
if (Math.abs(res) < EPS) { res = 0; }
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
function intersectSegments$1(a, b) {
|
|
// Note: this is almost the same as geom.intersectSegments()
|
|
// The main difference is that we don't have a pre-computed
|
|
// value for dx/dy on the segments.
|
|
// https://stackoverflow.com/a/1968345/125351
|
|
var aStart = a.from, bStart = b.from;
|
|
var p0_x = aStart.x, p0_y = aStart.y,
|
|
p2_x = bStart.x, p2_y = bStart.y;
|
|
|
|
var s1_x = a.from.x - a.to.x, s1_y = a.from.y - a.to.y, s2_x = b.from.x - b.to.x, s2_y = b.from.y - b.to.y;
|
|
var div = s1_x * s2_y - s2_x * s1_y;
|
|
|
|
var s = (s1_y * (p0_x - p2_x) - s1_x * (p0_y - p2_y)) / div;
|
|
if (s < 0 || s > 1) { return; }
|
|
|
|
var t = (s2_x * (p2_y - p0_y) + s2_y * (p0_x - p2_x)) / div;
|
|
|
|
if (t >= 0 && t <= 1) {
|
|
return {
|
|
x: p0_x - (t * s1_x),
|
|
y: p0_y - (t * s1_y)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a brute force solution with O(n^2) performance.
|
|
* (`n` is number of segments).
|
|
*
|
|
* Use this when number of lines is low, and number of intersections
|
|
* is high.
|
|
*/
|
|
function brute(lines, options) {
|
|
var results = [];
|
|
var reportIntersection = (options && options.onFound) ||
|
|
defaultIntersectionReporter;
|
|
var asyncState;
|
|
|
|
return {
|
|
/**
|
|
* Execute brute force of the segment intersection search
|
|
*/
|
|
run: run,
|
|
/**
|
|
* Access to results array. Works only when you use default onFound() handler
|
|
*/
|
|
results: results,
|
|
|
|
/**
|
|
* Performs a single step in the brute force algorithm ()
|
|
*/
|
|
step: step
|
|
}
|
|
|
|
function step() {
|
|
if (!asyncState) {
|
|
asyncState = {
|
|
i: 0
|
|
};
|
|
}
|
|
var test = lines[asyncState.i];
|
|
for (var j = asyncState.i + 1; j < lines.length; ++j) {
|
|
var other = lines[j];
|
|
var pt = intersectSegments$1(test, other);
|
|
if (pt) {
|
|
if (reportIntersection(pt, [test, other])) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
asyncState.i += 1;
|
|
return asyncState.i < lines.length;
|
|
}
|
|
|
|
function run() {
|
|
for(var i = 0; i < lines.length; ++i) {
|
|
var test = lines[i];
|
|
for (var j = i + 1; j < lines.length; ++j) {
|
|
var other = lines[j];
|
|
var pt = intersectSegments$1(test, other);
|
|
if (pt) {
|
|
if (reportIntersection(pt, [test, other])) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function defaultIntersectionReporter(p, interior) {
|
|
results.push({
|
|
point: p,
|
|
segments: interior
|
|
});
|
|
}
|
|
}
|
|
|
|
var ARRAY_TYPES = [
|
|
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
|
|
Int32Array, Uint32Array, Float32Array, Float64Array
|
|
];
|
|
|
|
var VERSION = 3; // serialized format version
|
|
|
|
var Flatbush = function Flatbush(numItems, nodeSize, ArrayType, data) {
|
|
var this$1 = this;
|
|
|
|
if (numItems === undefined) { throw new Error('Missing required argument: numItems.'); }
|
|
if (isNaN(numItems) || numItems <= 0) { throw new Error(("Unpexpected numItems value: " + numItems + ".")); }
|
|
|
|
this.numItems = +numItems;
|
|
this.nodeSize = Math.min(Math.max(+nodeSize || 16, 2), 65535);
|
|
|
|
// calculate the total number of nodes in the R-tree to allocate space for
|
|
// and the index of each tree level (used in search later)
|
|
var n = numItems;
|
|
var numNodes = n;
|
|
this._levelBounds = [n * 4];
|
|
do {
|
|
n = Math.ceil(n / this$1.nodeSize);
|
|
numNodes += n;
|
|
this$1._levelBounds.push(numNodes * 4);
|
|
} while (n !== 1);
|
|
|
|
this.ArrayType = ArrayType || Float64Array;
|
|
this.IndexArrayType = numNodes < 16384 ? Uint16Array : Uint32Array;
|
|
|
|
var arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
|
|
var nodesByteSize = numNodes * 4 * this.ArrayType.BYTES_PER_ELEMENT;
|
|
|
|
if (arrayTypeIndex < 0) {
|
|
throw new Error(("Unexpected typed array class: " + ArrayType + "."));
|
|
}
|
|
|
|
if (data && (data instanceof ArrayBuffer)) {
|
|
this.data = data;
|
|
this._boxes = new this.ArrayType(this.data, 8, numNodes * 4);
|
|
this._indices = new this.IndexArrayType(this.data, 8 + nodesByteSize, numNodes);
|
|
|
|
this._pos = numNodes * 4;
|
|
this.minX = this._boxes[this._pos - 4];
|
|
this.minY = this._boxes[this._pos - 3];
|
|
this.maxX = this._boxes[this._pos - 2];
|
|
this.maxY = this._boxes[this._pos - 1];
|
|
|
|
} else {
|
|
this.data = new ArrayBuffer(8 + nodesByteSize + numNodes * this.IndexArrayType.BYTES_PER_ELEMENT);
|
|
this._boxes = new this.ArrayType(this.data, 8, numNodes * 4);
|
|
this._indices = new this.IndexArrayType(this.data, 8 + nodesByteSize, numNodes);
|
|
this._pos = 0;
|
|
this.minX = Infinity;
|
|
this.minY = Infinity;
|
|
this.maxX = -Infinity;
|
|
this.maxY = -Infinity;
|
|
|
|
new Uint8Array(this.data, 0, 2).set([0xfb, (VERSION << 4) + arrayTypeIndex]);
|
|
new Uint16Array(this.data, 2, 1)[0] = nodeSize;
|
|
new Uint32Array(this.data, 4, 1)[0] = numItems;
|
|
}
|
|
};
|
|
|
|
Flatbush.from = function from (data) {
|
|
if (!(data instanceof ArrayBuffer)) {
|
|
throw new Error('Data must be an instance of ArrayBuffer.');
|
|
}
|
|
var ref = new Uint8Array(data, 0, 2);
|
|
var magic = ref[0];
|
|
var versionAndType = ref[1];
|
|
if (magic !== 0xfb) {
|
|
throw new Error('Data does not appear to be in a Flatbush format.');
|
|
}
|
|
if (versionAndType >> 4 !== VERSION) {
|
|
throw new Error(("Got v" + (versionAndType >> 4) + " data when expected v" + VERSION + "."));
|
|
}
|
|
var ref$1 = new Uint16Array(data, 2, 1);
|
|
var nodeSize = ref$1[0];
|
|
var ref$2 = new Uint32Array(data, 4, 1);
|
|
var numItems = ref$2[0];
|
|
|
|
return new Flatbush(numItems, nodeSize, ARRAY_TYPES[versionAndType & 0x0f], data);
|
|
};
|
|
|
|
Flatbush.prototype.add = function add (minX, minY, maxX, maxY) {
|
|
var index = this._pos >> 2;
|
|
this._indices[index] = index;
|
|
this._boxes[this._pos++] = minX;
|
|
this._boxes[this._pos++] = minY;
|
|
this._boxes[this._pos++] = maxX;
|
|
this._boxes[this._pos++] = maxY;
|
|
|
|
if (minX < this.minX) { this.minX = minX; }
|
|
if (minY < this.minY) { this.minY = minY; }
|
|
if (maxX > this.maxX) { this.maxX = maxX; }
|
|
if (maxY > this.maxY) { this.maxY = maxY; }
|
|
};
|
|
|
|
Flatbush.prototype.finish = function finish () {
|
|
var this$1 = this;
|
|
|
|
if (this._pos >> 2 !== this.numItems) {
|
|
throw new Error(("Added " + (this._pos >> 2) + " items when expected " + (this.numItems) + "."));
|
|
}
|
|
|
|
var width = this.maxX - this.minX;
|
|
var height = this.maxY - this.minY;
|
|
var hilbertValues = new Uint32Array(this.numItems);
|
|
var hilbertMax = (1 << 16) - 1;
|
|
|
|
// map item centers into Hilbert coordinate space and calculate Hilbert values
|
|
for (var i = 0; i < this.numItems; i++) {
|
|
var pos = 4 * i;
|
|
var minX = this$1._boxes[pos++];
|
|
var minY = this$1._boxes[pos++];
|
|
var maxX = this$1._boxes[pos++];
|
|
var maxY = this$1._boxes[pos++];
|
|
var x = Math.floor(hilbertMax * ((minX + maxX) / 2 - this$1.minX) / width);
|
|
var y = Math.floor(hilbertMax * ((minY + maxY) / 2 - this$1.minY) / height);
|
|
hilbertValues[i] = hilbert(x, y);
|
|
}
|
|
|
|
// sort items by their Hilbert value (for packing later)
|
|
sort$1(hilbertValues, this._boxes, this._indices, 0, this.numItems - 1);
|
|
|
|
// generate nodes at each tree level, bottom-up
|
|
for (var i$1 = 0, pos$1 = 0; i$1 < this._levelBounds.length - 1; i$1++) {
|
|
var end = this$1._levelBounds[i$1];
|
|
|
|
// generate a parent node for each block of consecutive <nodeSize> nodes
|
|
while (pos$1 < end) {
|
|
var nodeMinX = Infinity;
|
|
var nodeMinY = Infinity;
|
|
var nodeMaxX = -Infinity;
|
|
var nodeMaxY = -Infinity;
|
|
var nodeIndex = pos$1;
|
|
|
|
// calculate bbox for the new node
|
|
for (var i$2 = 0; i$2 < this.nodeSize && pos$1 < end; i$2++) {
|
|
var minX$1 = this$1._boxes[pos$1++];
|
|
var minY$1 = this$1._boxes[pos$1++];
|
|
var maxX$1 = this$1._boxes[pos$1++];
|
|
var maxY$1 = this$1._boxes[pos$1++];
|
|
if (minX$1 < nodeMinX) { nodeMinX = minX$1; }
|
|
if (minY$1 < nodeMinY) { nodeMinY = minY$1; }
|
|
if (maxX$1 > nodeMaxX) { nodeMaxX = maxX$1; }
|
|
if (maxY$1 > nodeMaxY) { nodeMaxY = maxY$1; }
|
|
}
|
|
|
|
// add the new node to the tree data
|
|
this$1._indices[this$1._pos >> 2] = nodeIndex;
|
|
this$1._boxes[this$1._pos++] = nodeMinX;
|
|
this$1._boxes[this$1._pos++] = nodeMinY;
|
|
this$1._boxes[this$1._pos++] = nodeMaxX;
|
|
this$1._boxes[this$1._pos++] = nodeMaxY;
|
|
}
|
|
}
|
|
};
|
|
|
|
Flatbush.prototype.search = function search (minX, minY, maxX, maxY, filterFn) {
|
|
var this$1 = this;
|
|
|
|
if (this._pos !== this._boxes.length) {
|
|
throw new Error('Data not yet indexed - call index.finish().');
|
|
}
|
|
|
|
var nodeIndex = this._boxes.length - 4;
|
|
var level = this._levelBounds.length - 1;
|
|
var queue = [];
|
|
var results = [];
|
|
|
|
while (nodeIndex !== undefined) {
|
|
// find the end index of the node
|
|
var end = Math.min(nodeIndex + this$1.nodeSize * 4, this$1._levelBounds[level]);
|
|
|
|
// search through child nodes
|
|
for (var pos = nodeIndex; pos < end; pos += 4) {
|
|
var index = this$1._indices[pos >> 2];
|
|
|
|
// check if node bbox intersects with query bbox
|
|
if (maxX < this$1._boxes[pos]) { continue; } // maxX < nodeMinX
|
|
if (maxY < this$1._boxes[pos + 1]) { continue; } // maxY < nodeMinY
|
|
if (minX > this$1._boxes[pos + 2]) { continue; } // minX > nodeMaxX
|
|
if (minY > this$1._boxes[pos + 3]) { continue; } // minY > nodeMaxY
|
|
|
|
if (nodeIndex < this$1.numItems * 4) {
|
|
if (filterFn === undefined || filterFn(index)) {
|
|
results.push(index); // leaf item
|
|
}
|
|
|
|
} else {
|
|
queue.push(index); // node; add it to the search queue
|
|
queue.push(level - 1);
|
|
}
|
|
}
|
|
|
|
level = queue.pop();
|
|
nodeIndex = queue.pop();
|
|
}
|
|
|
|
return results;
|
|
};
|
|
|
|
// custom quicksort that sorts bbox data alongside the hilbert values
|
|
function sort$1(values, boxes, indices, left, right) {
|
|
if (left >= right) { return; }
|
|
|
|
var pivot = values[(left + right) >> 1];
|
|
var i = left - 1;
|
|
var j = right + 1;
|
|
|
|
while (true) {
|
|
do { i++; } while (values[i] < pivot);
|
|
do { j--; } while (values[j] > pivot);
|
|
if (i >= j) { break; }
|
|
swap(values, boxes, indices, i, j);
|
|
}
|
|
|
|
sort$1(values, boxes, indices, left, j);
|
|
sort$1(values, boxes, indices, j + 1, right);
|
|
}
|
|
|
|
// swap two values and two corresponding boxes
|
|
function swap(values, boxes, indices, i, j) {
|
|
var temp = values[i];
|
|
values[i] = values[j];
|
|
values[j] = temp;
|
|
|
|
var k = 4 * i;
|
|
var m = 4 * j;
|
|
|
|
var a = boxes[k];
|
|
var b = boxes[k + 1];
|
|
var c = boxes[k + 2];
|
|
var d = boxes[k + 3];
|
|
boxes[k] = boxes[m];
|
|
boxes[k + 1] = boxes[m + 1];
|
|
boxes[k + 2] = boxes[m + 2];
|
|
boxes[k + 3] = boxes[m + 3];
|
|
boxes[m] = a;
|
|
boxes[m + 1] = b;
|
|
boxes[m + 2] = c;
|
|
boxes[m + 3] = d;
|
|
|
|
var e = indices[i];
|
|
indices[i] = indices[j];
|
|
indices[j] = e;
|
|
}
|
|
|
|
// Fast Hilbert curve algorithm by http://threadlocalmutex.com/
|
|
// Ported from C++ https://github.com/rawrunprotected/hilbert_curves (public domain)
|
|
function hilbert(x, y) {
|
|
var a = x ^ y;
|
|
var b = 0xFFFF ^ a;
|
|
var c = 0xFFFF ^ (x | y);
|
|
var d = x & (y ^ 0xFFFF);
|
|
|
|
var A = a | (b >> 1);
|
|
var B = (a >> 1) ^ a;
|
|
var C = ((c >> 1) ^ (b & (d >> 1))) ^ c;
|
|
var D = ((a & (c >> 1)) ^ (d >> 1)) ^ d;
|
|
|
|
a = A; b = B; c = C; d = D;
|
|
A = ((a & (a >> 2)) ^ (b & (b >> 2)));
|
|
B = ((a & (b >> 2)) ^ (b & ((a ^ b) >> 2)));
|
|
C ^= ((a & (c >> 2)) ^ (b & (d >> 2)));
|
|
D ^= ((b & (c >> 2)) ^ ((a ^ b) & (d >> 2)));
|
|
|
|
a = A; b = B; c = C; d = D;
|
|
A = ((a & (a >> 4)) ^ (b & (b >> 4)));
|
|
B = ((a & (b >> 4)) ^ (b & ((a ^ b) >> 4)));
|
|
C ^= ((a & (c >> 4)) ^ (b & (d >> 4)));
|
|
D ^= ((b & (c >> 4)) ^ ((a ^ b) & (d >> 4)));
|
|
|
|
a = A; b = B; c = C; d = D;
|
|
C ^= ((a & (c >> 8)) ^ (b & (d >> 8)));
|
|
D ^= ((b & (c >> 8)) ^ ((a ^ b) & (d >> 8)));
|
|
|
|
a = C ^ (C >> 1);
|
|
b = D ^ (D >> 1);
|
|
|
|
var i0 = x ^ y;
|
|
var i1 = b | (0xFFFF ^ (i0 | a));
|
|
|
|
i0 = (i0 | (i0 << 8)) & 0x00FF00FF;
|
|
i0 = (i0 | (i0 << 4)) & 0x0F0F0F0F;
|
|
i0 = (i0 | (i0 << 2)) & 0x33333333;
|
|
i0 = (i0 | (i0 << 1)) & 0x55555555;
|
|
|
|
i1 = (i1 | (i1 << 8)) & 0x00FF00FF;
|
|
i1 = (i1 | (i1 << 4)) & 0x0F0F0F0F;
|
|
i1 = (i1 | (i1 << 2)) & 0x33333333;
|
|
i1 = (i1 | (i1 << 1)) & 0x55555555;
|
|
|
|
return ((i1 << 1) | i0) >>> 0;
|
|
}
|
|
|
|
/**
|
|
* This implementation is inspired by discussion here
|
|
* https://twitter.com/mourner/status/1049325199617921024 and
|
|
* here https://github.com/anvaka/isect/issues/1
|
|
*
|
|
* It builds an index of all segments using static spatial index
|
|
* and then for each segment it queries overlapping rectangles.
|
|
*/
|
|
function bush(lines, options) {
|
|
var results = [];
|
|
var reportIntersection = (options && options.onFound) ||
|
|
defaultIntersectionReporter;
|
|
var asyncState;
|
|
|
|
var index = new Flatbush(lines.length);
|
|
lines.forEach(addToIndex);
|
|
index.finish();
|
|
|
|
return {
|
|
run: run,
|
|
step: step,
|
|
results: results,
|
|
|
|
// undocumented, don't use unless you know what you are doing:
|
|
checkIntersection: checkIntersection
|
|
}
|
|
|
|
function run() {
|
|
for (var i = 0; i < lines.length; ++i) {
|
|
if (checkIntersection(lines[i], i)) {
|
|
return; // stop early
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function checkIntersection(currentSegment, currentId) {
|
|
// sorry about code duplication.
|
|
var minX = currentSegment.from.x; var maxX = currentSegment.to.x;
|
|
var minY = currentSegment.from.y; var maxY = currentSegment.to.y;
|
|
var t;
|
|
if (minX > maxX) { t = minX; minX = maxX; maxX = t; }
|
|
if (minY > maxY) { t = minY; minY = maxY; maxY = t; }
|
|
|
|
var ids = index.search(minX, minY, maxX, maxY);
|
|
|
|
for (var i = 0; i < ids.length; ++i) {
|
|
var segmentIndex = ids[i];
|
|
if (segmentIndex <= currentId) { continue; } // we have either reported it, or it is current.
|
|
|
|
var otherSegment = lines[segmentIndex];
|
|
var point = intersectSegments$1(otherSegment, currentSegment);
|
|
|
|
if (point) {
|
|
if (reportIntersection(point, [currentSegment, otherSegment])) {
|
|
// stop early
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function step() {
|
|
if (!asyncState) {
|
|
asyncState = {i: 0};
|
|
}
|
|
var test = lines[asyncState.i];
|
|
checkIntersection(test, asyncState.i);
|
|
asyncState.i += 1;
|
|
return asyncState.i < lines.length;
|
|
}
|
|
|
|
|
|
function addToIndex(line) {
|
|
var minX = line.from.x; var maxX = line.to.x;
|
|
var minY = line.from.y; var maxY = line.to.y;
|
|
var t;
|
|
if (minX > maxX) { t = minX; minX = maxX; maxX = t; }
|
|
if (minY > maxY) { t = minY; minY = maxY; maxY = t; }
|
|
index.add(minX, minY, maxX, maxY);
|
|
}
|
|
|
|
function defaultIntersectionReporter(p, interior) {
|
|
results.push({
|
|
point: p,
|
|
segments: interior
|
|
});
|
|
}
|
|
}
|
|
|
|
export { isect as sweep, brute, bush };
|