From b01d6692a979c457195eb3b02756f78e4f57ec06 Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:15:14 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- js/rtree/rtree.js | 490 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 js/rtree/rtree.js diff --git a/js/rtree/rtree.js b/js/rtree/rtree.js new file mode 100644 index 0000000..96f2eb7 --- /dev/null +++ b/js/rtree/rtree.js @@ -0,0 +1,490 @@ +'use strict'; +var rectangle = Require('rtree/rectangle'); +var geojson = Require('rtree/geojson'); + + +function RTree(width) { + if (!(this instanceof RTree)) { + return new RTree(width); + } + // Variables to control tree-dimensions + var minWidth = 3; // Minimum width of any node before a merge + var maxWidth = 6; // Maximum width of any node before a split + if (!isNaN(width)) { + minWidth = Math.floor(width / 2.0); + maxWidth = width; + } + // Start with an empty root-tree + var rootTree = {x: 0, y: 0, w: 0, h: 0, id: 'root', nodes: [] }; + this.root = rootTree; + + + // This is my special addition to the world of r-trees + // every other (simple) method I found produced crap trees + // this skews insertions to prefering squarer and emptier nodes + var flatten = function (tree) { + var todo = tree.slice(); + var done = []; + var current; + while (todo.length) { + current = todo.pop(); + if (current.nodes) { + todo = todo.concat(current.nodes); + } else if (current.leaf) { + done.push(current); + } + } + return done; + }; + /* find the best specific node(s) for object to be deleted from + * [ leaf node parent ] = removeSubtree(rectangle, object, root) + * @private + */ + var removeSubtree = function (rect, obj, root) { + var hitStack = []; // Contains the elements that overlap + var countStack = []; // Contains the elements that overlap + var retArray = []; + var currentDepth = 1; + var tree, i, ltree; + if (!rect || !rectangle.overlapRectangle(rect, root)) { + return retArray; + } + var retObj = {x: rect.x, y: rect.y, w: rect.w, h: rect.h, target: obj}; + + countStack.push(root.nodes.length); + hitStack.push(root); + while (hitStack.length > 0) { + tree = hitStack.pop(); + i = countStack.pop() - 1; + if ('target' in retObj) { // will this ever be false? + while (i >= 0) { + ltree = tree.nodes[i]; + if (rectangle.overlapRectangle(retObj, ltree)) { + if ((retObj.target && 'leaf' in ltree && ltree.leaf === retObj.target) || (!retObj.target && ('leaf' in ltree || rectangle.containsRectangle(ltree, retObj)))) { + // A Match !! + // Yup we found a match... + // we can cancel search and start walking up the list + if ('nodes' in ltree) {// If we are deleting a node not a leaf... + retArray = flatten(tree.nodes.splice(i, 1)); + } else { + retArray = tree.nodes.splice(i, 1); + } + // Resize MBR down... + rectangle.makeMBR(tree.nodes, tree); + delete retObj.target; + //if (tree.nodes.length < minWidth) { // Underflow + // retObj.nodes = searchSubtree(tree, true, [], tree); + //} + break; + } else if ('nodes' in ltree) { // Not a Leaf + currentDepth++; + countStack.push(i); + hitStack.push(tree); + tree = ltree; + i = ltree.nodes.length; + } + } + i--; + } + + } else if ('nodes' in retObj) { // We are unsplitting + + tree.nodes.splice(i + 1, 1); // Remove unsplit node + if (tree.nodes.length > 0) { + rectangle.makeMBR(tree.nodes, tree); + } + for (var t = 0;t < retObj.nodes.length;t++) { + insertSubtree(retObj.nodes[t], tree); + } + retObj.nodes = []; + if (hitStack.length === 0 && tree.nodes.length <= 1) { // Underflow..on root! + retObj.nodes = searchSubtree(tree, true, retObj.nodes, tree); + tree.nodes = []; + hitStack.push(tree); + countStack.push(1); + } else if (hitStack.length > 0 && tree.nodes.length < minWidth) { // Underflow..AGAIN! + retObj.nodes = searchSubtree(tree, true, retObj.nodes, tree); + tree.nodes = []; + } else { + delete retObj.nodes; // Just start resizing + } + } else { // we are just resizing + rectangle.makeMBR(tree.nodes, tree); + } + currentDepth -= 1; + } + return retArray; + }; + + /* choose the best damn node for rectangle to be inserted into + * [ leaf node parent ] = chooseLeafSubtree(rectangle, root to start search at) + * @private + */ + var chooseLeafSubtree = function (rect, root) { + var bestChoiceIndex = -1; + var bestChoiceStack = []; + var bestChoiceArea; + var first = true; + bestChoiceStack.push(root); + var nodes = root.nodes; + + while (first || bestChoiceIndex !== -1) { + if (first) { + first = false; + } else { + bestChoiceStack.push(nodes[bestChoiceIndex]); + nodes = nodes[bestChoiceIndex].nodes; + bestChoiceIndex = -1; + } + + for (var i = nodes.length - 1; i >= 0; i--) { + var ltree = nodes[i]; + if ('leaf' in ltree) { + // Bail out of everything and start inserting + bestChoiceIndex = -1; + break; + } + // Area of new enlarged rectangle + var oldLRatio = rectangle.squarifiedRatio(ltree.w, ltree.h, ltree.nodes.length + 1); + + // Enlarge rectangle to fit new rectangle + var nw = Math.max(ltree.x + ltree.w, rect.x + rect.w) - Math.min(ltree.x, rect.x); + var nh = Math.max(ltree.y + ltree.h, rect.y + rect.h) - Math.min(ltree.y, rect.y); + + // Area of new enlarged rectangle + var lratio = rectangle.squarifiedRatio(nw, nh, ltree.nodes.length + 2); + + if (bestChoiceIndex < 0 || Math.abs(lratio - oldLRatio) < bestChoiceArea) { + bestChoiceArea = Math.abs(lratio - oldLRatio); + bestChoiceIndex = i; + } + } + } + + return bestChoiceStack; + }; + + /* split a set of nodes into two roughly equally-filled nodes + * [ an array of two new arrays of nodes ] = linearSplit(array of nodes) + * @private + */ + var linearSplit = function (nodes) { + var n = pickLinear(nodes); + while (nodes.length > 0) { + pickNext(nodes, n[0], n[1]); + } + return n; + }; + + /* insert the best source rectangle into the best fitting parent node: a or b + * [] = pickNext(array of source nodes, target node array a, target node array b) + * @private + */ + var pickNext = function (nodes, a, b) { + // Area of new enlarged rectangle + var areaA = rectangle.squarifiedRatio(a.w, a.h, a.nodes.length + 1); + var areaB = rectangle.squarifiedRatio(b.w, b.h, b.nodes.length + 1); + var highAreaDelta; + var highAreaNode; + var lowestGrowthGroup; + + for (var i = nodes.length - 1; i >= 0;i--) { + var l = nodes[i]; + var newAreaA = {}; + newAreaA.x = Math.min(a.x, l.x); + newAreaA.y = Math.min(a.y, l.y); + newAreaA.w = Math.max(a.x + a.w, l.x + l.w) - newAreaA.x; + newAreaA.h = Math.max(a.y + a.h, l.y + l.h) - newAreaA.y; + var changeNewAreaA = Math.abs(rectangle.squarifiedRatio(newAreaA.w, newAreaA.h, a.nodes.length + 2) - areaA); + + var newAreaB = {}; + newAreaB.x = Math.min(b.x, l.x); + newAreaB.y = Math.min(b.y, l.y); + newAreaB.w = Math.max(b.x + b.w, l.x + l.w) - newAreaB.x; + newAreaB.h = Math.max(b.y + b.h, l.y + l.h) - newAreaB.y; + var changeNewAreaB = Math.abs(rectangle.squarifiedRatio(newAreaB.w, newAreaB.h, b.nodes.length + 2) - areaB); + + if (!highAreaNode || !highAreaDelta || Math.abs(changeNewAreaB - changeNewAreaA) < highAreaDelta) { + highAreaNode = i; + highAreaDelta = Math.abs(changeNewAreaB - changeNewAreaA); + lowestGrowthGroup = changeNewAreaB < changeNewAreaA ? b : a; + } + } + var tempNode = nodes.splice(highAreaNode, 1)[0]; + if (a.nodes.length + nodes.length + 1 <= minWidth) { + a.nodes.push(tempNode); + rectangle.expandRectangle(a, tempNode); + } else if (b.nodes.length + nodes.length + 1 <= minWidth) { + b.nodes.push(tempNode); + rectangle.expandRectangle(b, tempNode); + } + else { + lowestGrowthGroup.nodes.push(tempNode); + rectangle.expandRectangle(lowestGrowthGroup, tempNode); + } + }; + + /* pick the 'best' two starter nodes to use as seeds using the 'linear' criteria + * [ an array of two new arrays of nodes ] = pickLinear(array of source nodes) + * @private + */ + var pickLinear = function (nodes) { + var lowestHighX = nodes.length - 1; + var highestLowX = 0; + var lowestHighY = nodes.length - 1; + var highestLowY = 0; + var t1, t2; + + for (var i = nodes.length - 2; i >= 0;i--) { + var l = nodes[i]; + if (l.x > nodes[highestLowX].x) { + highestLowX = i; + } else if (l.x + l.w < nodes[lowestHighX].x + nodes[lowestHighX].w) { + lowestHighX = i; + } + if (l.y > nodes[highestLowY].y) { + highestLowY = i; + } else if (l.y + l.h < nodes[lowestHighY].y + nodes[lowestHighY].h) { + lowestHighY = i; + } + } + var dx = Math.abs((nodes[lowestHighX].x + nodes[lowestHighX].w) - nodes[highestLowX].x); + var dy = Math.abs((nodes[lowestHighY].y + nodes[lowestHighY].h) - nodes[highestLowY].y); + if (dx > dy) { + if (lowestHighX > highestLowX) { + t1 = nodes.splice(lowestHighX, 1)[0]; + t2 = nodes.splice(highestLowX, 1)[0]; + } else { + t2 = nodes.splice(highestLowX, 1)[0]; + t1 = nodes.splice(lowestHighX, 1)[0]; + } + } else { + if (lowestHighY > highestLowY) { + t1 = nodes.splice(lowestHighY, 1)[0]; + t2 = nodes.splice(highestLowY, 1)[0]; + } else { + t2 = nodes.splice(highestLowY, 1)[0]; + t1 = nodes.splice(lowestHighY, 1)[0]; + } + } + return [ + {x: t1.x, y: t1.y, w: t1.w, h: t1.h, nodes: [t1]}, + {x: t2.x, y: t2.y, w: t2.w, h: t2.h, nodes: [t2]} + ]; + }; + + var attachData = function (node, moreTree) { + node.nodes = moreTree.nodes; + node.x = moreTree.x; + node.y = moreTree.y; + node.w = moreTree.w; + node.h = moreTree.h; + return node; + }; + + /* non-recursive internal search function + * [ nodes | objects ] = searchSubtree(rectangle, [return node data], [array to fill], root to begin search at) + * @private + */ + var searchSubtree = function (rect, returnNode, returnArray, root) { + var hitStack = []; // Contains the elements that overlap + + if (!rectangle.overlapRectangle(rect, root)) { + return returnArray; + } + + + hitStack.push(root.nodes); + + while (hitStack.length > 0) { + var nodes = hitStack.pop(); + + for (var i = nodes.length - 1; i >= 0; i--) { + var ltree = nodes[i]; + if (rectangle.overlapRectangle(rect, ltree)) { + if ('nodes' in ltree) { // Not a Leaf + hitStack.push(ltree.nodes); + } else if ('leaf' in ltree) { // A Leaf !! + if (!returnNode) { + returnArray.push(ltree.leaf); + } else { + returnArray.push(ltree); + } + } + } + } + } + + return returnArray; + }; + + /* non-recursive internal insert function + * [] = insertSubtree(rectangle, object to insert, root to begin insertion at) + * @private + */ + var insertSubtree = function (node, root) { + var bc; // Best Current node + // Initial insertion is special because we resize the Tree and we don't + // care about any overflow (seriously, how can the first object overflow?) + if (root.nodes.length === 0) { + root.x = node.x; + root.y = node.y; + root.w = node.w; + root.h = node.h; + root.nodes.push(node); + return; + } + + // Find the best fitting leaf node + // chooseLeaf returns an array of all tree levels (including root) + // that were traversed while trying to find the leaf + var treeStack = chooseLeafSubtree(node, root); + var retObj = node;//{x:rect.x,y:rect.y,w:rect.w,h:rect.h, leaf:obj}; + var pbc; + // Walk back up the tree resizing and inserting as needed + while (treeStack.length > 0) { + //handle the case of an empty node (from a split) + if (bc && 'nodes' in bc && bc.nodes.length === 0) { + pbc = bc; // Past bc + bc = treeStack.pop(); + for (var t = 0;t < bc.nodes.length;t++) { + if (bc.nodes[t] === pbc || bc.nodes[t].nodes.length === 0) { + bc.nodes.splice(t, 1); + break; + } + } + } else { + bc = treeStack.pop(); + } + + // If there is data attached to this retObj + if ('leaf' in retObj || 'nodes' in retObj || Array.isArray(retObj)) { + // Do Insert + if (Array.isArray(retObj)) { + for (var ai = 0; ai < retObj.length; ai++) { + rectangle.expandRectangle(bc, retObj[ai]); + } + bc.nodes = bc.nodes.concat(retObj); + } else { + rectangle.expandRectangle(bc, retObj); + bc.nodes.push(retObj); // Do Insert + } + + if (bc.nodes.length <= maxWidth) { // Start Resizeing Up the Tree + retObj = {x: bc.x, y: bc.y, w: bc.w, h: bc.h}; + } else { // Otherwise Split this Node + // linearSplit() returns an array containing two new nodes + // formed from the split of the previous node's overflow + var a = linearSplit(bc.nodes); + retObj = a;//[1]; + + if (treeStack.length < 1) { // If are splitting the root.. + bc.nodes.push(a[0]); + treeStack.push(bc); // Reconsider the root element + retObj = a[1]; + } /*else { + delete bc; + }*/ + } + } else { // Otherwise Do Resize + //Just keep applying the new bounding rectangle to the parents.. + rectangle.expandRectangle(bc, retObj); + retObj = {x: bc.x, y: bc.y, w: bc.w, h: bc.h}; + } + } + }; + + this.insertSubtree = insertSubtree; + /* quick 'n' dirty function for plugins or manually drawing the tree + * [ tree ] = RTree.getTree(): returns the raw tree data. useful for adding + * @public + * !! DEPRECATED !! + */ + this.getTree = function () { + return rootTree; + }; + + /* quick 'n' dirty function for plugins or manually loading the tree + * [ tree ] = RTree.setTree(sub-tree, where to attach): returns the raw tree data. useful for adding + * @public + * !! DEPRECATED !! + */ + this.setTree = function (newTree, where) { + if (!where) { + where = rootTree; + } + return attachData(where, newTree); + }; + + /* non-recursive search function + * [ nodes | objects ] = RTree.search(rectangle, [return node data], [array to fill]) + * @public + */ + this.search = function (rect, returnNode, returnArray) { + returnArray = returnArray || []; + return searchSubtree(rect, returnNode, returnArray, rootTree); + }; + + + var removeArea = function (rect) { + var numberDeleted = 1, + retArray = [], + deleted; + while (numberDeleted > 0) { + deleted = removeSubtree(rect, false, rootTree); + numberDeleted = deleted.length; + retArray = retArray.concat(deleted); + } + return retArray; + }; + + var removeObj = function (rect, obj) { + var retArray = removeSubtree(rect, obj, rootTree); + return retArray; + }; + /* non-recursive delete function + * [deleted object] = RTree.remove(rectangle, [object to delete]) + */ + this.remove = function (rect, obj) { + if (!obj || typeof obj === 'function') { + return removeArea(rect, obj); + } else { + return removeObj(rect, obj); + } + }; + + /* non-recursive insert function + * [] = RTree.insert(rectangle, object to insert) + */ + this.insert = function (rect, obj) { + var retArray = insertSubtree({x: rect.x, y: rect.y, w: rect.w, h: rect.h, leaf: obj}, rootTree); + return retArray; + }; +} +RTree.prototype.toJSON = function (printing) { + return JSON.stringify(this.root, false, printing); +}; + +RTree.fromJSON = function (json) { + var rt = new RTree(); + rt.setTree(JSON.parse(json)); + return rt; +}; + +RTree.prototype.bbox = geojson.bbox; +RTree.prototype.geoJSON = geojson.geoJSON; +RTree.Rectangle = rectangle; + +module.exports = RTree; + + +/** + * Polyfill for the Array.isArray function + * todo: Test on IE7 and IE8 + * Taken from https://github.com/geraintluff/tv4/issues/20 + */ +if (typeof Array.isArray !== 'function') { + Array.isArray = function (a) { + return typeof a === 'object' && {}.toString.call(a) === '[object Array]'; + }; +}