UNPKG

12.9 kBJavaScriptView Raw
1"use strict";
2exports.__esModule = true;
3/* eslint no-nested-ternary: 0 */
4/* eslint no-constant-condition: 0 */
5/**
6 * Sigma.js Quad Tree Class
7 * =========================
8 *
9 * Class implementing the quad tree data structure used to solve hovers and
10 * determine which elements are currently in the scope of the camera so that
11 * we don't waste time rendering things the user cannot see anyway.
12 */
13var extend_1 = require("@yomguithereal/helpers/extend");
14// TODO: should not ask the quadtree when the camera has the whole graph in
15// sight.
16// TODO: a square can be represented as topleft + width, saying for the quad blocks (reduce mem)
17// TODO: jsdoc
18// TODO: be sure we can handle cases overcoming boundaries (because of size) or use a maxed size
19// TODO: filtering unwanted labels beforehand through the filter function
20// NOTE: this is basically a MX-CIF Quadtree at this point
21// NOTE: need to explore R-Trees for edges
22// NOTE: need to explore 2d segment tree for edges
23// NOTE: probably can do faster using spatial hashing
24/**
25 * Constants.
26 *
27 * Note that since we are representing a static 4-ary tree, the indices of the
28 * quadrants are the following:
29 * - TOP_LEFT: 4i + b
30 * - TOP_RIGHT: 4i + 2b
31 * - BOTTOM_LEFT: 4i + 3b
32 * - BOTTOM_RIGHT: 4i + 4b
33 */
34var BLOCKS = 4, MAX_LEVEL = 5;
35var X_OFFSET = 0, Y_OFFSET = 1, WIDTH_OFFSET = 2, HEIGHT_OFFSET = 3;
36var TOP_LEFT = 1, TOP_RIGHT = 2, BOTTOM_LEFT = 3, BOTTOM_RIGHT = 4;
37/**
38 * Geometry helpers.
39 */
40/**
41 * Function returning whether the given rectangle is axis-aligned.
42 *
43 * @param {number} x1
44 * @param {number} y1
45 * @param {number} x2
46 * @param {number} y2
47 * @return {boolean}
48 */
49function isAxisAligned(x1, y1, x2, y2) {
50 return x1 === x2 || y1 === y2;
51}
52function squareCollidesWithQuad(x1, y1, w, qx, qy, qw, qh) {
53 return x1 < qx + qw && x1 + w > qx && y1 < qy + qh && y1 + w > qy;
54}
55function rectangleCollidesWithQuad(x1, y1, w, h, qx, qy, qw, qh) {
56 return x1 < qx + qw && x1 + w > qx && y1 < qy + qh && y1 + h > qy;
57}
58function pointIsInQuad(x, y, qx, qy, qw, qh) {
59 var xmp = qx + qw / 2, ymp = qy + qh / 2, top = y < ymp, left = x < xmp;
60 return top ? (left ? TOP_LEFT : TOP_RIGHT) : left ? BOTTOM_LEFT : BOTTOM_RIGHT;
61}
62/**
63 * Helper functions that are not bound to the class so an external user
64 * cannot mess with them.
65 */
66function buildQuadrants(maxLevel, data) {
67 // [block, level]
68 var stack = [0, 0];
69 while (stack.length) {
70 var level = stack.pop(), block = stack.pop();
71 var topLeftBlock = 4 * block + BLOCKS, topRightBlock = 4 * block + 2 * BLOCKS, bottomLeftBlock = 4 * block + 3 * BLOCKS, bottomRightBlock = 4 * block + 4 * BLOCKS;
72 var x = data[block + X_OFFSET], y = data[block + Y_OFFSET], width = data[block + WIDTH_OFFSET], height = data[block + HEIGHT_OFFSET], hw = width / 2, hh = height / 2;
73 data[topLeftBlock + X_OFFSET] = x;
74 data[topLeftBlock + Y_OFFSET] = y;
75 data[topLeftBlock + WIDTH_OFFSET] = hw;
76 data[topLeftBlock + HEIGHT_OFFSET] = hh;
77 data[topRightBlock + X_OFFSET] = x + hw;
78 data[topRightBlock + Y_OFFSET] = y;
79 data[topRightBlock + WIDTH_OFFSET] = hw;
80 data[topRightBlock + HEIGHT_OFFSET] = hh;
81 data[bottomLeftBlock + X_OFFSET] = x;
82 data[bottomLeftBlock + Y_OFFSET] = y + hh;
83 data[bottomLeftBlock + WIDTH_OFFSET] = hw;
84 data[bottomLeftBlock + HEIGHT_OFFSET] = hh;
85 data[bottomRightBlock + X_OFFSET] = x + hw;
86 data[bottomRightBlock + Y_OFFSET] = y + hh;
87 data[bottomRightBlock + WIDTH_OFFSET] = hw;
88 data[bottomRightBlock + HEIGHT_OFFSET] = hh;
89 if (level < maxLevel - 1) {
90 stack.push(bottomRightBlock, level + 1);
91 stack.push(bottomLeftBlock, level + 1);
92 stack.push(topRightBlock, level + 1);
93 stack.push(topLeftBlock, level + 1);
94 }
95 }
96}
97function insertNode(maxLevel, data, containers, key, x, y, size) {
98 var x1 = x - size, y1 = y - size, w = size * 2;
99 var level = 0, block = 0;
100 while (true) {
101 // If we reached max level
102 if (level >= maxLevel) {
103 containers[block] = containers[block] || [];
104 containers[block].push(key);
105 return;
106 }
107 var topLeftBlock = 4 * block + BLOCKS, topRightBlock = 4 * block + 2 * BLOCKS, bottomLeftBlock = 4 * block + 3 * BLOCKS, bottomRightBlock = 4 * block + 4 * BLOCKS;
108 var collidingWithTopLeft = squareCollidesWithQuad(x1, y1, w, data[topLeftBlock + X_OFFSET], data[topLeftBlock + Y_OFFSET], data[topLeftBlock + WIDTH_OFFSET], data[topLeftBlock + HEIGHT_OFFSET]);
109 var collidingWithTopRight = squareCollidesWithQuad(x1, y1, w, data[topRightBlock + X_OFFSET], data[topRightBlock + Y_OFFSET], data[topRightBlock + WIDTH_OFFSET], data[topRightBlock + HEIGHT_OFFSET]);
110 var collidingWithBottomLeft = squareCollidesWithQuad(x1, y1, w, data[bottomLeftBlock + X_OFFSET], data[bottomLeftBlock + Y_OFFSET], data[bottomLeftBlock + WIDTH_OFFSET], data[bottomLeftBlock + HEIGHT_OFFSET]);
111 var collidingWithBottomRight = squareCollidesWithQuad(x1, y1, w, data[bottomRightBlock + X_OFFSET], data[bottomRightBlock + Y_OFFSET], data[bottomRightBlock + WIDTH_OFFSET], data[bottomRightBlock + HEIGHT_OFFSET]);
112 var collisions = [
113 collidingWithTopLeft,
114 collidingWithTopRight,
115 collidingWithBottomLeft,
116 collidingWithBottomRight,
117 ].reduce(function (acc, current) {
118 if (current)
119 return acc + 1;
120 else
121 return acc;
122 }, 0);
123 // If we don't have at least a collision, there is an issue
124 if (collisions === 0)
125 throw new Error("sigma/quadtree.insertNode: no collision (level: " + level + ", key: " + key + ", x: " + x + ", y: " + y + ", size: " + size + ").");
126 // If we have 3 collisions, we have a geometry problem obviously
127 if (collisions === 3)
128 throw new Error("sigma/quadtree.insertNode: 3 impossible collisions (level: " + level + ", key: " + key + ", x: " + x + ", y: " + y + ", size: " + size + ").");
129 // If we have more that one collision, we stop here and store the node
130 // in the relevant containers
131 if (collisions > 1) {
132 // NOTE: this is a nice way to optimize for hover, but not for frustum
133 // since it requires to uniq the collected nodes
134 // if (collisions < 4) {
135 // // If we intersect two quads, we place the node in those two
136 // if (collidingWithTopLeft) {
137 // containers[topLeftBlock] = containers[topLeftBlock] || [];
138 // containers[topLeftBlock].push(key);
139 // }
140 // if (collidingWithTopRight) {
141 // containers[topRightBlock] = containers[topRightBlock] || [];
142 // containers[topRightBlock].push(key);
143 // }
144 // if (collidingWithBottomLeft) {
145 // containers[bottomLeftBlock] = containers[bottomLeftBlock] || [];
146 // containers[bottomLeftBlock].push(key);
147 // }
148 // if (collidingWithBottomRight) {
149 // containers[bottomRightBlock] = containers[bottomRightBlock] || [];
150 // containers[bottomRightBlock].push(key);
151 // }
152 // }
153 // else {
154 // // Else we keep the node where it is to avoid more pointless computations
155 // containers[block] = containers[block] || [];
156 // containers[block].push(key);
157 // }
158 containers[block] = containers[block] || [];
159 containers[block].push(key);
160 return;
161 }
162 else {
163 level++;
164 }
165 // Else we recurse into the correct quads
166 if (collidingWithTopLeft)
167 block = topLeftBlock;
168 if (collidingWithTopRight)
169 block = topRightBlock;
170 if (collidingWithBottomLeft)
171 block = bottomLeftBlock;
172 if (collidingWithBottomRight)
173 block = bottomRightBlock;
174 }
175}
176function getNodesInAxisAlignedRectangleArea(maxLevel, data, containers, x1, y1, w, h) {
177 // [block, level]
178 var stack = [0, 0];
179 var collectedNodes = [];
180 var container;
181 while (stack.length) {
182 var level = stack.pop(), block = stack.pop();
183 // Collecting nodes
184 container = containers[block];
185 if (container)
186 extend_1["default"](collectedNodes, container);
187 // If we reached max level
188 if (level >= maxLevel)
189 continue;
190 var topLeftBlock = 4 * block + BLOCKS, topRightBlock = 4 * block + 2 * BLOCKS, bottomLeftBlock = 4 * block + 3 * BLOCKS, bottomRightBlock = 4 * block + 4 * BLOCKS;
191 var collidingWithTopLeft = rectangleCollidesWithQuad(x1, y1, w, h, data[topLeftBlock + X_OFFSET], data[topLeftBlock + Y_OFFSET], data[topLeftBlock + WIDTH_OFFSET], data[topLeftBlock + HEIGHT_OFFSET]);
192 var collidingWithTopRight = rectangleCollidesWithQuad(x1, y1, w, h, data[topRightBlock + X_OFFSET], data[topRightBlock + Y_OFFSET], data[topRightBlock + WIDTH_OFFSET], data[topRightBlock + HEIGHT_OFFSET]);
193 var collidingWithBottomLeft = rectangleCollidesWithQuad(x1, y1, w, h, data[bottomLeftBlock + X_OFFSET], data[bottomLeftBlock + Y_OFFSET], data[bottomLeftBlock + WIDTH_OFFSET], data[bottomLeftBlock + HEIGHT_OFFSET]);
194 var collidingWithBottomRight = rectangleCollidesWithQuad(x1, y1, w, h, data[bottomRightBlock + X_OFFSET], data[bottomRightBlock + Y_OFFSET], data[bottomRightBlock + WIDTH_OFFSET], data[bottomRightBlock + HEIGHT_OFFSET]);
195 if (collidingWithTopLeft)
196 stack.push(topLeftBlock, level + 1);
197 if (collidingWithTopRight)
198 stack.push(topRightBlock, level + 1);
199 if (collidingWithBottomLeft)
200 stack.push(bottomLeftBlock, level + 1);
201 if (collidingWithBottomRight)
202 stack.push(bottomRightBlock, level + 1);
203 }
204 return collectedNodes;
205}
206/**
207 * QuadTree class.
208 *
209 * @constructor
210 * @param {object} boundaries - The graph boundaries.
211 */
212var QuadTree = /** @class */ (function () {
213 function QuadTree(params) {
214 if (params === void 0) { params = {}; }
215 this.containers = {};
216 this.cache = null;
217 this.lastRectangle = null;
218 // Allocating the underlying byte array
219 var L = Math.pow(4, MAX_LEVEL);
220 this.data = new Float32Array(BLOCKS * ((4 * L - 1) / 3));
221 if (params.boundaries)
222 this.resize(params.boundaries);
223 else
224 this.resize({
225 x: 0,
226 y: 0,
227 width: 1,
228 height: 1
229 });
230 if (typeof params.filter === "function")
231 this.nodeFilter = params.filter;
232 }
233 QuadTree.prototype.add = function (key, x, y, size) {
234 insertNode(MAX_LEVEL, this.data, this.containers, key, x, y, size);
235 return this;
236 };
237 QuadTree.prototype.resize = function (boundaries) {
238 this.clear();
239 // Building the quadrants
240 this.data[X_OFFSET] = boundaries.x;
241 this.data[Y_OFFSET] = boundaries.y;
242 this.data[WIDTH_OFFSET] = boundaries.width;
243 this.data[HEIGHT_OFFSET] = boundaries.height;
244 buildQuadrants(MAX_LEVEL, this.data);
245 };
246 QuadTree.prototype.clear = function () {
247 this.containers = {};
248 return this;
249 };
250 QuadTree.prototype.point = function (x, y) {
251 var nodes = [];
252 var block = 0, level = 0;
253 do {
254 if (this.containers[block])
255 nodes.push.apply(nodes, this.containers[block]);
256 var quad = pointIsInQuad(x, y, this.data[block + X_OFFSET], this.data[block + Y_OFFSET], this.data[block + WIDTH_OFFSET], this.data[block + HEIGHT_OFFSET]);
257 block = 4 * block + quad * BLOCKS;
258 level++;
259 } while (level <= MAX_LEVEL);
260 return nodes;
261 };
262 QuadTree.prototype.rectangle = function (x1, y1, x2, y2, height) {
263 var lr = this.lastRectangle;
264 if (lr && x1 === lr.x1 && x2 === lr.x2 && y1 === lr.y1 && y2 === lr.y2 && height === lr.height) {
265 return this.cache;
266 }
267 this.lastRectangle = {
268 x1: x1,
269 y1: y1,
270 x2: x2,
271 y2: y2,
272 height: height
273 };
274 // Is the rectangle axis aligned?
275 if (!isAxisAligned(x1, y1, x2, y2))
276 throw new Error("sigma/quadtree.rectangle: shifted view is not yet implemented.");
277 var collectedNodes = getNodesInAxisAlignedRectangleArea(MAX_LEVEL, this.data, this.containers, x1, y1, Math.abs(x1 - x2) || Math.abs(y1 - y2), height);
278 this.cache = collectedNodes;
279 return this.cache;
280 };
281 return QuadTree;
282}());
283exports["default"] = QuadTree;