From a76cfdd012dbef460b962302bd9ba8de90a65d95 Mon Sep 17 00:00:00 2001 From: Kangning Li Date: Thu, 29 Mar 2018 18:52:55 -0400 Subject: [PATCH] add batching for ground primitives --- .../gallery/batchingGroundPrims.html | 22 +- LICENSE.md | 32 + Source/Core/RectangleRbush.js | 70 +++ Source/DataSources/GeometryVisualizer.js | 4 +- .../StaticGroundGeometryPerMaterialBatch.js | 36 +- Source/Scene/GroundPrimitive.js | 17 + Source/ThirdParty/quickselect.js | 59 ++ Source/ThirdParty/rbush.js | 561 ++++++++++++++++++ 8 files changed, 775 insertions(+), 26 deletions(-) create mode 100644 Source/Core/RectangleRbush.js create mode 100644 Source/ThirdParty/quickselect.js create mode 100644 Source/ThirdParty/rbush.js diff --git a/Apps/Sandcastle/gallery/batchingGroundPrims.html b/Apps/Sandcastle/gallery/batchingGroundPrims.html index dccf66dbad4f..f4d551db8079 100644 --- a/Apps/Sandcastle/gallery/batchingGroundPrims.html +++ b/Apps/Sandcastle/gallery/batchingGroundPrims.html @@ -78,7 +78,7 @@ new Cesium.Cartesian3(-2352875.095159641, -3742564.819171856, 4582173.540953957), new Cesium.Cartesian3(-2350669.646050987, -3743751.6823160048, 4582334.8406995395) ]; - +/* // concave polygon var redPolygon1 = viewer.entities.add({ name : 'concave polygon on surface', @@ -87,7 +87,7 @@ material : '../images/Cesium_Logo_Color.jpg' } }); - +*/ // polygons with non-overlapping extents seem to be batchable without problems /* var redPolygon1 = viewer.entities.add({ @@ -136,7 +136,7 @@ });*/ // nearly overlapping rectangles over mt. st. helens -/* + var latitude = 46.1922; var longitude = -122.1934; @@ -167,7 +167,12 @@ material : Cesium.Color.YELLOW.withAlpha(0.5) } }); -*/ + +var checkerboard = new Cesium.CheckerboardMaterialProperty({ + evenColor : Cesium.Color.ORANGE, + oddColor : Cesium.Color.YELLOW, + repeat : new Cesium.Cartesian2(14, 14) +}); var rightHandler = new Cesium.ScreenSpaceEventHandler(scene.canvas); rightHandler.setInputAction(function(movement) { @@ -181,17 +186,12 @@ var normalized = Cesium.Cartesian3.normalize(cartesian, cartesian); console.log(normalized); - viewer.entities.removeAll(); + //viewer.entities.removeAll(); viewer.entities.add({ name : lat + ' ' + long, rectangle : { coordinates : Cesium.Rectangle.fromDegrees(long - 0.0002, lat - 0.0001, long + 0.0002, lat + 0.0001), - material : - new Cesium.CheckerboardMaterialProperty({ - evenColor : Cesium.Color.ORANGE, - oddColor : Cesium.Color.YELLOW, - repeat : new Cesium.Cartesian2(14, 14) - }) + material : checkerboard } }); } diff --git a/LICENSE.md b/LICENSE.md index fc5c2e459829..0c5435f75a6c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -509,6 +509,38 @@ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +### rbush + +https://github.com/mourner/rbush + +> MIT License + +> Copyright (c) 2016 Vladimir Agafonkin + +>Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +> +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +> +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +### quickselect + +https://github.com/mourner/quickselect + +No license given, used by rbush + ### crunch https://github.com/BinomialLLC/crunch diff --git a/Source/Core/RectangleRbush.js b/Source/Core/RectangleRbush.js new file mode 100644 index 000000000000..3525d8eca9e6 --- /dev/null +++ b/Source/Core/RectangleRbush.js @@ -0,0 +1,70 @@ +define([ + '../ThirdParty/rbush', + './Check' + ], function( + rbush, + Check) { + 'use strict'; + + /** + * Wrapper around rbush for use with Rectangle types. + * @private + */ + function RectangleRbush() { + this._tree = rbush(); + } + + function RectangleWithId() { + this.minX = 0.0; + this.minY = 0.0; + this.maxX = 0.0; + this.maxY = 0.0; + this.id = ''; + } + + function fromRectangleAndId(rectangle, id, result) { + result.minX = rectangle.west; + result.minY = rectangle.south; + result.maxX = rectangle.east; + result.maxY = rectangle.north; + result.id = id; + return result; + } + + function idCompare(a, b) { + return a.id === b.id; + } + + RectangleRbush.prototype.insert = function(id, rectangle) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('id', id); + Check.typeOf.object('rectangle', rectangle); + //>>includeEnd('debug'); + + var withId = fromRectangleAndId(rectangle, id, new RectangleWithId()); + this._tree.insert(withId); + }; + + var removalScratch = new RectangleWithId(); + RectangleRbush.prototype.remove = function(id, rectangle) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('id', id); + Check.typeOf.object('rectangle', rectangle); + //>>includeEnd('debug'); + + var withId = fromRectangleAndId(rectangle, id, removalScratch); + this._tree.remove(withId, idCompare); + }; + + var collisionScratch = new RectangleWithId(); + RectangleRbush.prototype.collides = function(rectangle) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('rectangle', rectangle); + //>>includeEnd('debug'); + + var withId = fromRectangleAndId(rectangle, '', collisionScratch); + return this._tree.collides(withId); + }; + + return RectangleRbush; +}); diff --git a/Source/DataSources/GeometryVisualizer.js b/Source/DataSources/GeometryVisualizer.js index b59357ab7161..d6f271a73342 100644 --- a/Source/DataSources/GeometryVisualizer.js +++ b/Source/DataSources/GeometryVisualizer.js @@ -25,7 +25,6 @@ define([ './RectangleGeometryUpdater', './StaticGeometryColorBatch', './StaticGeometryPerMaterialBatch', - './StaticGroundGeometryColorBatch', './StaticGroundGeometryPerMaterialBatch', './StaticOutlineGeometryBatch', './WallGeometryUpdater' @@ -56,7 +55,6 @@ define([ RectangleGeometryUpdater, StaticGeometryColorBatch, StaticGeometryPerMaterialBatch, - StaticGroundGeometryColorBatch, StaticGroundGeometryPerMaterialBatch, StaticOutlineGeometryBatch, WallGeometryUpdater) { @@ -160,7 +158,7 @@ define([ this._groundMaterialBatches = new Array(numberOfClassificationTypes); // TODO: why is this? for (i = 0; i < numberOfClassificationTypes; ++i) { - this._groundColorBatches[i] = new StaticGroundGeometryColorBatch(groundPrimitives, PerInstanceColorAppearance, i); + this._groundColorBatches[i] = new StaticGroundGeometryPerMaterialBatch(groundPrimitives, PerInstanceColorAppearance, i); this._groundMaterialBatches[i] = new StaticGroundGeometryPerMaterialBatch(groundPrimitives, MaterialAppearance, i); } diff --git a/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js b/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js index 85c2c61b2cab..9d4c92553548 100644 --- a/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js +++ b/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js @@ -6,6 +6,7 @@ define([ '../Core/DistanceDisplayCondition', '../Core/DistanceDisplayConditionGeometryInstanceAttribute', '../Core/ShowGeometryInstanceAttribute', + '../Core/RectangleRbush', '../Scene/GroundPrimitive', './BoundingSphereState', './ColorMaterialProperty', @@ -19,6 +20,7 @@ define([ DistanceDisplayCondition, DistanceDisplayConditionGeometryInstanceAttribute, ShowGeometryInstanceAttribute, + RectangleRbush, GroundPrimitive, BoundingSphereState, ColorMaterialProperty, @@ -29,13 +31,13 @@ define([ var distanceDisplayConditionScratch = new DistanceDisplayCondition(); // Encapsulates a Primitive and all the entities that it represents. - function Batch(primitives, appearanceType, materialProperty, shadows) { + function Batch(primitives, appearanceType, materialProperty, shadows, usingSphericalCoordinates) { this.primitives = primitives; // scene level primitive collection this.appearanceType = appearanceType; this.materialProperty = materialProperty; this.updaters = new AssociativeArray(); this.createPrimitive = true; - this.primitive = undefined; + this.primitive = undefined; // a GroundPrimitive encapsulating all the entities this.oldPrimitive = undefined; this.geometry = new AssociativeArray(); this.material = undefined; @@ -46,30 +48,34 @@ define([ this.subscriptions = new AssociativeArray(); this.showsUpdated = new AssociativeArray(); this.shadows = shadows; + this.usingSphericalCoordinates = usingSphericalCoordinates; + this.rbush = new RectangleRbush(); } Batch.prototype.onMaterialChanged = function() { this.invalidated = true; }; - Batch.prototype.nonOverlapping = function(updater) { - return false; + Batch.prototype.nonOverlapping = function(rectangle) { + return !this.rbush.collides(rectangle); }; Batch.prototype.isMaterial = function(updater) { var material = this.materialProperty; var updaterMaterial = updater.fillMaterialProperty; - if (updaterMaterial === material) { + if (updaterMaterial === material || + (updaterMaterial instanceof ColorMaterialProperty && material instanceof ColorMaterialProperty)) { return true; } return defined(material) && material.equals(updaterMaterial); }; - Batch.prototype.add = function(time, updater) { + Batch.prototype.add = function(time, updater, geometryInstance) { var id = updater.id; this.updaters.set(id, updater); - this.geometry.set(id, updater.createFillGeometryInstance(time)); + this.geometry.set(id, geometryInstance); + this.rbush.insert(id, geometryInstance.geometry.rectangle); if (!updater.hasConstantFill || !updater.fillMaterialProperty.isConstant || !Property.isConstant(updater.distanceDisplayConditionProperty)) { this.updatersWithAttributes.set(id, updater); } else { @@ -85,8 +91,10 @@ define([ Batch.prototype.remove = function(updater) { var id = updater.id; + var geometryInstance = this.geometry.get(id); this.createPrimitive = this.geometry.remove(id) || this.createPrimitive; if (this.updaters.remove(id)) { + this.rbush.remove(id, geometryInstance.geometry.rectangle); this.updatersWithAttributes.remove(id); var unsubscribe = this.subscriptions.get(id); if (defined(unsubscribe)) { @@ -309,15 +317,19 @@ define([ StaticGroundGeometryPerMaterialBatch.prototype.add = function(time, updater) { var items = this._items; var length = items.length; - for (var i = 0; i < length; i++) { + var geometryInstance = updater.createFillGeometryInstance(time); + var usingSphericalCoordinates = GroundPrimitive.shouldUseSphericalCoordinates(geometryInstance.geometry.rectangle); + for (var i = 0; i < length; ++i) { var item = items[i]; - if (item.isMaterial(updater) && item.nonOverlapping(updater)) { - item.add(time, updater); + if (item.isMaterial(updater) && + item.nonOverlapping(geometryInstance.geometry.rectangle) && + item.usingSphericalCoordinates === usingSphericalCoordinates) { + item.add(time, updater, geometryInstance); return; } } - var batch = new Batch(this._primitives, this._appearanceType, updater.fillMaterialProperty, this._shadows); - batch.add(time, updater); + var batch = new Batch(this._primitives, this._appearanceType, updater.fillMaterialProperty, this._shadows, usingSphericalCoordinates); + batch.add(time, updater, geometryInstance); items.push(batch); }; diff --git a/Source/Scene/GroundPrimitive.js b/Source/Scene/GroundPrimitive.js index 60826cc9be9a..2c225658deb1 100644 --- a/Source/Scene/GroundPrimitive.js +++ b/Source/Scene/GroundPrimitive.js @@ -5,6 +5,7 @@ define([ '../Core/Cartesian3', '../Core/Cartesian4', '../Core/Cartographic', + '../Core/Check', '../Core/defaultValue', '../Core/defined', '../Core/defineProperties', @@ -32,6 +33,7 @@ define([ Cartesian3, Cartesian4, Cartographic, + Check, defaultValue, defined, defineProperties, @@ -926,6 +928,21 @@ define([ return destroyObject(this); }; + /** + * Computes whether the given rectangle is wide enough that texture coordinates + * over its area should be computed using spherical extents instead of distance to planes. + * + * @param {Rectangle} rectangle A rectangle + * @private + */ + GroundPrimitive.shouldUseSphericalCoordinates = function(rectangle) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('rectangle', rectangle); + //>>includeEnd('debug'); + + return shouldUseSpherical(rectangle); + }; + /** * Texture coordinates for ground primitives are computed either using spherical coordinates for large areas or * using distance from planes for small areas. diff --git a/Source/ThirdParty/quickselect.js b/Source/ThirdParty/quickselect.js new file mode 100644 index 000000000000..a474563cc4fd --- /dev/null +++ b/Source/ThirdParty/quickselect.js @@ -0,0 +1,59 @@ +define([], function() { +'use strict'; + +function quickselect(arr, k, left, right, compare) { + quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare); +}; + +function quickselectStep(arr, k, left, right, compare) { + + while (right > left) { + if (right - left > 600) { + var n = right - left + 1; + var m = k - left + 1; + var z = Math.log(n); + var s = 0.5 * Math.exp(2 * z / 3); + var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); + var newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); + var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); + quickselectStep(arr, k, newLeft, newRight, compare); + } + + var t = arr[k]; + var i = left; + var j = right; + + swap(arr, left, k); + if (compare(arr[right], t) > 0) swap(arr, left, right); + + while (i < j) { + swap(arr, i, j); + i++; + j--; + while (compare(arr[i], t) < 0) i++; + while (compare(arr[j], t) > 0) j--; + } + + if (compare(arr[left], t) === 0) swap(arr, left, j); + else { + j++; + swap(arr, j, right); + } + + if (j <= k) left = j + 1; + if (k <= j) right = j - 1; + } +} + +function swap(arr, i, j) { + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} + +function defaultCompare(a, b) { + return a < b ? -1 : a > b ? 1 : 0; +} + +return quickselect; +}); diff --git a/Source/ThirdParty/rbush.js b/Source/ThirdParty/rbush.js new file mode 100644 index 000000000000..bbb7b6fbc61b --- /dev/null +++ b/Source/ThirdParty/rbush.js @@ -0,0 +1,561 @@ +define(['./quickselect'], function(quickselect) { +'use strict'; + +function rbush(maxEntries, format) { + if (!(this instanceof rbush)) return new rbush(maxEntries, format); + + // max entries in a node is 9 by default; min node fill is 40% for best performance + this._maxEntries = Math.max(4, maxEntries || 9); + this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4)); + + if (format) { + this._initFormat(format); + } + + this.clear(); +} + +rbush.prototype = { + + all: function () { + return this._all(this.data, []); + }, + + search: function (bbox) { + + var node = this.data, + result = [], + toBBox = this.toBBox; + + if (!intersects(bbox, node)) return result; + + var nodesToSearch = [], + i, len, child, childBBox; + + while (node) { + for (i = 0, len = node.children.length; i < len; i++) { + + child = node.children[i]; + childBBox = node.leaf ? toBBox(child) : child; + + if (intersects(bbox, childBBox)) { + if (node.leaf) result.push(child); + else if (contains(bbox, childBBox)) this._all(child, result); + else nodesToSearch.push(child); + } + } + node = nodesToSearch.pop(); + } + + return result; + }, + + collides: function (bbox) { + + var node = this.data, + toBBox = this.toBBox; + + if (!intersects(bbox, node)) return false; + + var nodesToSearch = [], + i, len, child, childBBox; + + while (node) { + for (i = 0, len = node.children.length; i < len; i++) { + + child = node.children[i]; + childBBox = node.leaf ? toBBox(child) : child; + + if (intersects(bbox, childBBox)) { + if (node.leaf || contains(bbox, childBBox)) return true; + nodesToSearch.push(child); + } + } + node = nodesToSearch.pop(); + } + + return false; + }, + + load: function (data) { + if (!(data && data.length)) return this; + + if (data.length < this._minEntries) { + for (var i = 0, len = data.length; i < len; i++) { + this.insert(data[i]); + } + return this; + } + + // recursively build the tree with the given data from scratch using OMT algorithm + var node = this._build(data.slice(), 0, data.length - 1, 0); + + if (!this.data.children.length) { + // save as is if tree is empty + this.data = node; + + } else if (this.data.height === node.height) { + // split root if trees have the same height + this._splitRoot(this.data, node); + + } else { + if (this.data.height < node.height) { + // swap trees if inserted one is bigger + var tmpNode = this.data; + this.data = node; + node = tmpNode; + } + + // insert the small tree into the large tree at appropriate level + this._insert(node, this.data.height - node.height - 1, true); + } + + return this; + }, + + insert: function (item) { + if (item) this._insert(item, this.data.height - 1); + return this; + }, + + clear: function () { + this.data = createNode([]); + return this; + }, + + remove: function (item, equalsFn) { + if (!item) return this; + + var node = this.data, + bbox = this.toBBox(item), + path = [], + indexes = [], + i, parent, index, goingUp; + + // depth-first iterative tree traversal + while (node || path.length) { + + if (!node) { // go up + node = path.pop(); + parent = path[path.length - 1]; + i = indexes.pop(); + goingUp = true; + } + + if (node.leaf) { // check current node + index = findItem(item, node.children, equalsFn); + + if (index !== -1) { + // item found, remove the item and condense tree upwards + node.children.splice(index, 1); + path.push(node); + this._condense(path); + return this; + } + } + + if (!goingUp && !node.leaf && contains(node, bbox)) { // go down + path.push(node); + indexes.push(i); + i = 0; + parent = node; + node = node.children[0]; + + } else if (parent) { // go right + i++; + node = parent.children[i]; + goingUp = false; + + } else node = null; // nothing found + } + + return this; + }, + + toBBox: function (item) { return item; }, + + compareMinX: compareNodeMinX, + compareMinY: compareNodeMinY, + + toJSON: function () { return this.data; }, + + fromJSON: function (data) { + this.data = data; + return this; + }, + + _all: function (node, result) { + var nodesToSearch = []; + while (node) { + if (node.leaf) result.push.apply(result, node.children); + else nodesToSearch.push.apply(nodesToSearch, node.children); + + node = nodesToSearch.pop(); + } + return result; + }, + + _build: function (items, left, right, height) { + + var N = right - left + 1, + M = this._maxEntries, + node; + + if (N <= M) { + // reached leaf level; return leaf + node = createNode(items.slice(left, right + 1)); + calcBBox(node, this.toBBox); + return node; + } + + if (!height) { + // target height of the bulk-loaded tree + height = Math.ceil(Math.log(N) / Math.log(M)); + + // target number of root entries to maximize storage utilization + M = Math.ceil(N / Math.pow(M, height - 1)); + } + + node = createNode([]); + node.leaf = false; + node.height = height; + + // split the items into M mostly square tiles + + var N2 = Math.ceil(N / M), + N1 = N2 * Math.ceil(Math.sqrt(M)), + i, j, right2, right3; + + multiSelect(items, left, right, N1, this.compareMinX); + + for (i = left; i <= right; i += N1) { + + right2 = Math.min(i + N1 - 1, right); + + multiSelect(items, i, right2, N2, this.compareMinY); + + for (j = i; j <= right2; j += N2) { + + right3 = Math.min(j + N2 - 1, right2); + + // pack each entry recursively + node.children.push(this._build(items, j, right3, height - 1)); + } + } + + calcBBox(node, this.toBBox); + + return node; + }, + + _chooseSubtree: function (bbox, node, level, path) { + + var i, len, child, targetNode, area, enlargement, minArea, minEnlargement; + + while (true) { + path.push(node); + + if (node.leaf || path.length - 1 === level) break; + + minArea = minEnlargement = Infinity; + + for (i = 0, len = node.children.length; i < len; i++) { + child = node.children[i]; + area = bboxArea(child); + enlargement = enlargedArea(bbox, child) - area; + + // choose entry with the least area enlargement + if (enlargement < minEnlargement) { + minEnlargement = enlargement; + minArea = area < minArea ? area : minArea; + targetNode = child; + + } else if (enlargement === minEnlargement) { + // otherwise choose one with the smallest area + if (area < minArea) { + minArea = area; + targetNode = child; + } + } + } + + node = targetNode || node.children[0]; + } + + return node; + }, + + _insert: function (item, level, isNode) { + + var toBBox = this.toBBox, + bbox = isNode ? item : toBBox(item), + insertPath = []; + + // find the best node for accommodating the item, saving all nodes along the path too + var node = this._chooseSubtree(bbox, this.data, level, insertPath); + + // put the item into the node + node.children.push(item); + extend(node, bbox); + + // split on node overflow; propagate upwards if necessary + while (level >= 0) { + if (insertPath[level].children.length > this._maxEntries) { + this._split(insertPath, level); + level--; + } else break; + } + + // adjust bboxes along the insertion path + this._adjustParentBBoxes(bbox, insertPath, level); + }, + + // split overflowed node into two + _split: function (insertPath, level) { + + var node = insertPath[level], + M = node.children.length, + m = this._minEntries; + + this._chooseSplitAxis(node, m, M); + + var splitIndex = this._chooseSplitIndex(node, m, M); + + var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex)); + newNode.height = node.height; + newNode.leaf = node.leaf; + + calcBBox(node, this.toBBox); + calcBBox(newNode, this.toBBox); + + if (level) insertPath[level - 1].children.push(newNode); + else this._splitRoot(node, newNode); + }, + + _splitRoot: function (node, newNode) { + // split root node + this.data = createNode([node, newNode]); + this.data.height = node.height + 1; + this.data.leaf = false; + calcBBox(this.data, this.toBBox); + }, + + _chooseSplitIndex: function (node, m, M) { + + var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index; + + minOverlap = minArea = Infinity; + + for (i = m; i <= M - m; i++) { + bbox1 = distBBox(node, 0, i, this.toBBox); + bbox2 = distBBox(node, i, M, this.toBBox); + + overlap = intersectionArea(bbox1, bbox2); + area = bboxArea(bbox1) + bboxArea(bbox2); + + // choose distribution with minimum overlap + if (overlap < minOverlap) { + minOverlap = overlap; + index = i; + + minArea = area < minArea ? area : minArea; + + } else if (overlap === minOverlap) { + // otherwise choose distribution with minimum area + if (area < minArea) { + minArea = area; + index = i; + } + } + } + + return index; + }, + + // sorts node children by the best axis for split + _chooseSplitAxis: function (node, m, M) { + + var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX, + compareMinY = node.leaf ? this.compareMinY : compareNodeMinY, + xMargin = this._allDistMargin(node, m, M, compareMinX), + yMargin = this._allDistMargin(node, m, M, compareMinY); + + // if total distributions margin value is minimal for x, sort by minX, + // otherwise it's already sorted by minY + if (xMargin < yMargin) node.children.sort(compareMinX); + }, + + // total margin of all possible split distributions where each node is at least m full + _allDistMargin: function (node, m, M, compare) { + + node.children.sort(compare); + + var toBBox = this.toBBox, + leftBBox = distBBox(node, 0, m, toBBox), + rightBBox = distBBox(node, M - m, M, toBBox), + margin = bboxMargin(leftBBox) + bboxMargin(rightBBox), + i, child; + + for (i = m; i < M - m; i++) { + child = node.children[i]; + extend(leftBBox, node.leaf ? toBBox(child) : child); + margin += bboxMargin(leftBBox); + } + + for (i = M - m - 1; i >= m; i--) { + child = node.children[i]; + extend(rightBBox, node.leaf ? toBBox(child) : child); + margin += bboxMargin(rightBBox); + } + + return margin; + }, + + _adjustParentBBoxes: function (bbox, path, level) { + // adjust bboxes along the given tree path + for (var i = level; i >= 0; i--) { + extend(path[i], bbox); + } + }, + + _condense: function (path) { + // go through the path, removing empty nodes and updating bboxes + for (var i = path.length - 1, siblings; i >= 0; i--) { + if (path[i].children.length === 0) { + if (i > 0) { + siblings = path[i - 1].children; + siblings.splice(siblings.indexOf(path[i]), 1); + + } else this.clear(); + + } else calcBBox(path[i], this.toBBox); + } + }, + + _initFormat: function (format) { + // data format (minX, minY, maxX, maxY accessors) + + // uses eval-type function compilation instead of just accepting a toBBox function + // because the algorithms are very sensitive to sorting functions performance, + // so they should be dead simple and without inner calls + + var compareArr = ['return a', ' - b', ';']; + + this.compareMinX = new Function('a', 'b', compareArr.join(format[0])); + this.compareMinY = new Function('a', 'b', compareArr.join(format[1])); + + this.toBBox = new Function('a', + 'return {minX: a' + format[0] + + ', minY: a' + format[1] + + ', maxX: a' + format[2] + + ', maxY: a' + format[3] + '};'); + } +}; + +function findItem(item, items, equalsFn) { + if (!equalsFn) return items.indexOf(item); + + for (var i = 0; i < items.length; i++) { + if (equalsFn(item, items[i])) return i; + } + return -1; +} + +// calculate node's bbox from bboxes of its children +function calcBBox(node, toBBox) { + distBBox(node, 0, node.children.length, toBBox, node); +} + +// min bounding rectangle of node children from k to p-1 +function distBBox(node, k, p, toBBox, destNode) { + if (!destNode) destNode = createNode(null); + destNode.minX = Infinity; + destNode.minY = Infinity; + destNode.maxX = -Infinity; + destNode.maxY = -Infinity; + + for (var i = k, child; i < p; i++) { + child = node.children[i]; + extend(destNode, node.leaf ? toBBox(child) : child); + } + + return destNode; +} + +function extend(a, b) { + a.minX = Math.min(a.minX, b.minX); + a.minY = Math.min(a.minY, b.minY); + a.maxX = Math.max(a.maxX, b.maxX); + a.maxY = Math.max(a.maxY, b.maxY); + return a; +} + +function compareNodeMinX(a, b) { return a.minX - b.minX; } +function compareNodeMinY(a, b) { return a.minY - b.minY; } + +function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); } +function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); } + +function enlargedArea(a, b) { + return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) * + (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY)); +} + +function intersectionArea(a, b) { + var minX = Math.max(a.minX, b.minX), + minY = Math.max(a.minY, b.minY), + maxX = Math.min(a.maxX, b.maxX), + maxY = Math.min(a.maxY, b.maxY); + + return Math.max(0, maxX - minX) * + Math.max(0, maxY - minY); +} + +function contains(a, b) { + return a.minX <= b.minX && + a.minY <= b.minY && + b.maxX <= a.maxX && + b.maxY <= a.maxY; +} + +function intersects(a, b) { + return b.minX <= a.maxX && + b.minY <= a.maxY && + b.maxX >= a.minX && + b.maxY >= a.minY; +} + +function createNode(children) { + return { + children: children, + height: 1, + leaf: true, + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity + }; +} + +// sort an array so that items come in groups of n unsorted items, with groups sorted between each other; +// combines selection algorithm with binary divide & conquer approach + +function multiSelect(arr, left, right, n, compare) { + var stack = [left, right], + mid; + + while (stack.length) { + right = stack.pop(); + left = stack.pop(); + + if (right - left <= n) continue; + + mid = left + Math.ceil((right - left) / n / 2) * n; + quickselect(arr, mid, left, right, compare); + + stack.push(left, mid, mid, right); + } +} + +return rbush; +});