UNPKG

s2maps-gpu

Version:

S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.

218 lines (217 loc) 7.81 kB
import { projectX, projectY } from 'gis-tools/index.js'; /** * # Point Index * * A kd-tree based point index */ export default class PointIndex { nodeSize; points = []; #sorted = false; /** @param nodeSize - size of the kd-tree leaf node */ constructor(nodeSize = 64) { this.nodeSize = nodeSize; } /** * Add a point to the index. * @param x - x coordinate * @param y - y coordinate * @param data - data related to point to add */ add(x, y, data) { if (isNaN(x) || isNaN(y)) return; this.points.push({ x: projectX(x), y: projectY(y), data }); this.#sorted = false; } /** * Add a new point to the index * @param point - a point to add */ addPoint(point) { this.points.push(point); this.#sorted = false; } /** Perform indexing of the added points. */ sort() { if (this.#sorted) return; // kd-sort both arrays for efficient search this.#sort(this.nodeSize, 0, this.points.length - 1, 0); this.#sorted = true; } /** * Search the index for items within a given bounding box. * @param minX - minimum x value * @param minY - minimum y value * @param maxX - maximum x value * @param maxY - maximum y value * @returns All points found within the bounding box */ range(minX, minY, maxX, maxY) { this.sort(); const { nodeSize } = this; const stack = [ [0, this.points.length - 1, 0], ]; const result = []; // ids of items that are in range // recursively search for items in range in the kd-sorted arrays while (stack.length > 0) { const [left, right, axis] = stack.pop(); // if we reached "tree node", search linearly if (right - left <= nodeSize) { for (let i = left; i <= right; i++) { const point = this.points[i]; const { x, y } = point; if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(this.points[i]); } continue; } // otherwise find the middle index const m = (left + right) >> 1; // include the middle item if it's in range const { x, y } = this.points[m]; if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(this.points[m]); // queue search in halves that intersect the query if (axis === 0 ? minX <= x : minY <= y) stack.push([left, m - 1, 1 - axis]); if (axis === 0 ? maxX >= x : maxY >= y) stack.push([m + 1, right, 1 - axis]); } return result; } /** * Search the index for items within a given radius. * @param qx - query point X-coordinate * @param qy - query point Y-coordinate * @param r - radius * @returns All points found within the radius of the query point */ radius(qx, qy, r) { this.sort(); const { nodeSize, points } = this; const stack = [[0, points.length - 1, 0]]; // left, right, axis const result = []; // ids of items that are in range const r2 = r * r; // recursively search for items within radius in the kd-sorted arrays while (stack.length > 0) { const [left, right, axis] = stack.pop(); // if we reached "tree node", search linearly if (right - left <= nodeSize) { for (let i = left; i <= right; i++) { const point = points[i]; if (this.#sqDist(point.x, point.y, qx, qy) <= r2) result.push(point); } continue; } // otherwise find the middle index const m = (left + right) >> 1; // include the middle item if it's in range const point = points[m]; const { x, y } = point; if (this.#sqDist(x, y, qx, qy) <= r2) result.push(point); // queue search in halves that intersect the query if (axis === 0 ? qx - r <= x : qy - r <= y) stack.push([left, m - 1, 1 - axis]); if (axis === 0 ? qx + r >= x : qy + r >= y) stack.push([m + 1, right, 1 - axis]); } return result; } /** * Perform indexing/sorting of the added points. * @param nodeSize - tree node size * @param left - left index * @param right - right index * @param axis - axis (0 for x or 1 for y) */ #sort(nodeSize, left, right, axis) { if (right - left <= nodeSize) return; const m = (left + right) >> 1; // middle index // sort ids and coords around the middle index so that the halves lie // either left/right or top/bottom correspondingly (taking turns) this.select(m, left, right, axis); // recursively kd-sort first half and second half on the opposite axis this.#sort(nodeSize, left, m - 1, 1 - axis); this.#sort(nodeSize, m + 1, right, 1 - axis); } /** * Custom Floyd-Rivest selection algorithm: sort ids and coords so that * [left..k-1] items are smaller than k-th item (on either x or y axis) * @param k - index * @param left - left index * @param right - right index * @param axis - axis (0 for x or 1 for y) */ select(k, left, right, axis) { const { points } = this; while (right > left) { if (right - left > 600) { const n = right - left + 1; const m = k - left + 1; const z = Math.log(n); const s = 0.5 * Math.exp((2 * z) / 3); const sd = 0.5 * Math.sqrt((z * s * (n - s)) / n) * (m - n / 2 < 0 ? -1 : 1); const newLeft = Math.max(left, Math.floor(k - (m * s) / n + sd)); const newRight = Math.min(right, Math.floor(k + ((n - m) * s) / n + sd)); this.select(k, newLeft, newRight, axis); } const p = points[k]; const t = axis === 0 ? p.x : p.y; let i = left; let j = right; this.#swap(left, k); const rp = points[right]; if (axis === 0 ? rp.x > t : rp.y > t) this.#swap(left, right); while (i < j) { this.#swap(i, j); i++; j--; while ((axis === 0 ? points[i].x : points[i].y) < t) i++; while ((axis === 0 ? points[j].x : points[j].y) > t) j--; } const lp = points[left]; if ((axis === 0 ? lp.x : lp.y) === t) { this.#swap(left, j); } else { j++; this.#swap(j, right); } if (j <= k) left = j + 1; if (k <= j) right = j - 1; } } /** * Swap two points positions in the array * @param i - first index * @param j - second index */ #swap(i, j) { const { points } = this; [points[i], points[j]] = [points[j], points[i]]; } /** * Squared distance between two points * @param ax - point A x coordinate * @param ay - point A y coordinate * @param bx - point B x coordinate * @param by - point B y coordinate * @returns the squared distance */ #sqDist(ax, ay, bx, by) { const dx = ax - bx; const dy = ay - by; return dx * dx + dy * dy; } }