@gridworkjs/kd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/package.json +44 -0
- package/src/index.js +1 -0
- package/src/kd.js +349 -0
- package/types/index.d.ts +1 -0
- package/types/kd.d.ts +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.svg" width="256" height="256" alt="@gridworkjs/kd">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@gridworkjs/kd</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">KD-tree spatial index for static point sets and nearest-neighbor queries</p>
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm install @gridworkjs/kd
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { createKdTree } from '@gridworkjs/kd'
|
|
19
|
+
import { point, bounds } from '@gridworkjs/core'
|
|
20
|
+
|
|
21
|
+
// index a set of restaurants by location
|
|
22
|
+
const tree = createKdTree(r => bounds(r.location))
|
|
23
|
+
|
|
24
|
+
tree.load([
|
|
25
|
+
{ name: 'Corner Bistro', location: point(40.738, -74.005) },
|
|
26
|
+
{ name: 'Katz Deli', location: point(40.722, -73.987) },
|
|
27
|
+
{ name: 'Joe Pizza', location: point(40.730, -73.989) },
|
|
28
|
+
{ name: 'Russ & Daughters', location: point(40.722, -73.988) }
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
// find the 2 closest restaurants to your current location
|
|
32
|
+
tree.nearest({ x: 40.725, y: -73.990 }, 2)
|
|
33
|
+
// => [{ name: 'Joe Pizza', ... }, { name: 'Katz Deli', ... }]
|
|
34
|
+
|
|
35
|
+
// search a bounding box
|
|
36
|
+
tree.search({ minX: 40.720, minY: -73.990, maxX: 40.730, maxY: -73.985 })
|
|
37
|
+
// => [{ name: 'Katz Deli', ... }, { name: 'Russ & Daughters', ... }]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## When to Use a KD-tree
|
|
41
|
+
|
|
42
|
+
KD-trees excel at nearest-neighbor queries on point data. If you have a static dataset and your primary query is "find the k closest items to this point", a KD-tree will outperform other spatial indexes.
|
|
43
|
+
|
|
44
|
+
Use `load()` to build the tree from a complete dataset - this produces a balanced tree with optimal query performance. Dynamic `insert()` and `remove()` are supported but may unbalance the tree over time.
|
|
45
|
+
|
|
46
|
+
If your data changes frequently, consider `@gridworkjs/quadtree` (dynamic, sparse data) or `@gridworkjs/hashgrid` (uniform distributions). If your items are rectangles rather than points, `@gridworkjs/rtree` is a better fit.
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### `createKdTree(accessor)`
|
|
51
|
+
|
|
52
|
+
Creates a new KD-tree. The `accessor` function maps each item to its bounding box (`{ minX, minY, maxX, maxY }`). Use `bounds()` from `@gridworkjs/core` to convert geometries.
|
|
53
|
+
|
|
54
|
+
Returns a spatial index implementing the gridwork protocol.
|
|
55
|
+
|
|
56
|
+
### `tree.load(items)`
|
|
57
|
+
|
|
58
|
+
Builds a balanced tree from an array of items, replacing any existing data. This is the preferred way to populate the tree - it produces optimal structure for queries.
|
|
59
|
+
|
|
60
|
+
### `tree.insert(item)`
|
|
61
|
+
|
|
62
|
+
Adds a single item to the tree. For bulk data, prefer `load()`.
|
|
63
|
+
|
|
64
|
+
### `tree.remove(item)`
|
|
65
|
+
|
|
66
|
+
Removes an item by identity (`===`). Returns `true` if found and removed.
|
|
67
|
+
|
|
68
|
+
### `tree.search(query)`
|
|
69
|
+
|
|
70
|
+
Returns all items whose bounds intersect the query. Accepts bounds objects or geometry objects (point, rect, circle).
|
|
71
|
+
|
|
72
|
+
### `tree.nearest(point, k?)`
|
|
73
|
+
|
|
74
|
+
Returns the `k` nearest items to the given point, sorted by distance. Defaults to `k=1`. Accepts `{ x, y }` or a point geometry.
|
|
75
|
+
|
|
76
|
+
### `tree.clear()`
|
|
77
|
+
|
|
78
|
+
Removes all items from the tree.
|
|
79
|
+
|
|
80
|
+
### `tree.size`
|
|
81
|
+
|
|
82
|
+
Number of items in the tree.
|
|
83
|
+
|
|
84
|
+
### `tree.bounds`
|
|
85
|
+
|
|
86
|
+
Bounding box of all items, or `null` if empty.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gridworkjs/kd",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "KD-tree spatial index for static point sets and nearest-neighbor queries",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "./types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./types/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"prepublishOnly": "npx tsc --declaration --allowJs --emitDeclarationOnly --skipLibCheck --target es2020 --module nodenext --moduleResolution nodenext --strict false --esModuleInterop true --outDir ./types src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"kdtree",
|
|
20
|
+
"kd-tree",
|
|
21
|
+
"spatial",
|
|
22
|
+
"spatial-index",
|
|
23
|
+
"gridwork",
|
|
24
|
+
"nearest-neighbor",
|
|
25
|
+
"knn",
|
|
26
|
+
"point",
|
|
27
|
+
"search"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/gridworkjs/kd.git"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src/",
|
|
36
|
+
"types/"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@gridworkjs/core": "^1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createKdTree } from './kd.js'
|
package/src/kd.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SPATIAL_INDEX, bounds as toBounds,
|
|
3
|
+
intersects, distanceToPoint
|
|
4
|
+
} from '@gridworkjs/core'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{ minX: number, minY: number, maxX: number, maxY: number }} Bounds
|
|
8
|
+
* @typedef {{ x: number, y: number }} Point
|
|
9
|
+
* @typedef {(item: T) => Bounds} Accessor
|
|
10
|
+
* @template T
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function validateAccessorBounds(b) {
|
|
14
|
+
if (b === null || typeof b !== 'object') {
|
|
15
|
+
throw new Error('accessor must return a bounds object')
|
|
16
|
+
}
|
|
17
|
+
if (!Number.isFinite(b.minX) || !Number.isFinite(b.minY) ||
|
|
18
|
+
!Number.isFinite(b.maxX) || !Number.isFinite(b.maxY)) {
|
|
19
|
+
throw new Error('accessor returned non-finite bounds')
|
|
20
|
+
}
|
|
21
|
+
if (b.minX > b.maxX || b.minY > b.maxY) {
|
|
22
|
+
throw new Error('accessor returned inverted bounds (minX > maxX or minY > maxY)')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeBounds(input) {
|
|
27
|
+
if (input != null && typeof input === 'object' &&
|
|
28
|
+
'minX' in input && 'minY' in input && 'maxX' in input && 'maxY' in input) {
|
|
29
|
+
return input
|
|
30
|
+
}
|
|
31
|
+
return toBounds(input)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function centerDim(b, dim) {
|
|
35
|
+
return dim === 0 ? (b.minX + b.maxX) / 2 : (b.minY + b.maxY) / 2
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeNode(entry, depth) {
|
|
39
|
+
const dim = depth % 2
|
|
40
|
+
return {
|
|
41
|
+
entry,
|
|
42
|
+
splitDim: dim,
|
|
43
|
+
splitVal: centerDim(entry.bounds, dim),
|
|
44
|
+
subtreeBounds: { ...entry.bounds },
|
|
45
|
+
left: null,
|
|
46
|
+
right: null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function expandBounds(target, source) {
|
|
51
|
+
if (source.minX < target.minX) target.minX = source.minX
|
|
52
|
+
if (source.minY < target.minY) target.minY = source.minY
|
|
53
|
+
if (source.maxX > target.maxX) target.maxX = source.maxX
|
|
54
|
+
if (source.maxY > target.maxY) target.maxY = source.maxY
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recomputeSubtreeBounds(node) {
|
|
58
|
+
node.subtreeBounds = { ...node.entry.bounds }
|
|
59
|
+
if (node.left) expandBounds(node.subtreeBounds, node.left.subtreeBounds)
|
|
60
|
+
if (node.right) expandBounds(node.subtreeBounds, node.right.subtreeBounds)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildSubtree(entries, depth) {
|
|
64
|
+
if (entries.length === 0) return null
|
|
65
|
+
if (entries.length === 1) return makeNode(entries[0], depth)
|
|
66
|
+
|
|
67
|
+
const dim = depth % 2
|
|
68
|
+
entries.sort((a, b) => centerDim(a.bounds, dim) - centerDim(b.bounds, dim))
|
|
69
|
+
const mid = entries.length >> 1
|
|
70
|
+
|
|
71
|
+
const node = makeNode(entries[mid], depth)
|
|
72
|
+
node.left = buildSubtree(entries.slice(0, mid), depth + 1)
|
|
73
|
+
node.right = buildSubtree(entries.slice(mid + 1), depth + 1)
|
|
74
|
+
recomputeSubtreeBounds(node)
|
|
75
|
+
return node
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function insertInto(node, entry, depth) {
|
|
79
|
+
if (!node) return makeNode(entry, depth)
|
|
80
|
+
|
|
81
|
+
const val = centerDim(entry.bounds, node.splitDim)
|
|
82
|
+
if (val < node.splitVal) {
|
|
83
|
+
node.left = insertInto(node.left, entry, depth + 1)
|
|
84
|
+
} else {
|
|
85
|
+
node.right = insertInto(node.right, entry, depth + 1)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expandBounds(node.subtreeBounds, entry.bounds)
|
|
89
|
+
return node
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findMin(node, targetDim, depth) {
|
|
93
|
+
if (!node) return null
|
|
94
|
+
const dim = depth % 2
|
|
95
|
+
|
|
96
|
+
if (dim === targetDim) {
|
|
97
|
+
if (!node.left) return node.entry
|
|
98
|
+
return findMin(node.left, targetDim, depth + 1)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const leftMin = findMin(node.left, targetDim, depth + 1)
|
|
102
|
+
const rightMin = findMin(node.right, targetDim, depth + 1)
|
|
103
|
+
|
|
104
|
+
let best = node.entry
|
|
105
|
+
if (leftMin && centerDim(leftMin.bounds, targetDim) < centerDim(best.bounds, targetDim)) {
|
|
106
|
+
best = leftMin
|
|
107
|
+
}
|
|
108
|
+
if (rightMin && centerDim(rightMin.bounds, targetDim) < centerDim(best.bounds, targetDim)) {
|
|
109
|
+
best = rightMin
|
|
110
|
+
}
|
|
111
|
+
return best
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function removeFrom(node, item, itemBounds, depth) {
|
|
115
|
+
if (!node) return { node: null, found: false }
|
|
116
|
+
|
|
117
|
+
if (node.entry.item === item) {
|
|
118
|
+
if (!node.left && !node.right) return { node: null, found: true }
|
|
119
|
+
|
|
120
|
+
const dim = node.splitDim
|
|
121
|
+
if (node.right) {
|
|
122
|
+
const rep = findMin(node.right, dim, depth + 1)
|
|
123
|
+
node.entry = rep
|
|
124
|
+
node.splitVal = centerDim(rep.bounds, dim)
|
|
125
|
+
const r = removeFrom(node.right, rep.item, rep.bounds, depth + 1)
|
|
126
|
+
node.right = r.node
|
|
127
|
+
} else {
|
|
128
|
+
const rep = findMin(node.left, dim, depth + 1)
|
|
129
|
+
node.entry = rep
|
|
130
|
+
node.splitVal = centerDim(rep.bounds, dim)
|
|
131
|
+
const r = removeFrom(node.left, rep.item, rep.bounds, depth + 1)
|
|
132
|
+
node.right = r.node
|
|
133
|
+
node.left = null
|
|
134
|
+
}
|
|
135
|
+
recomputeSubtreeBounds(node)
|
|
136
|
+
return { node, found: true }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const val = centerDim(itemBounds, node.splitDim)
|
|
140
|
+
|
|
141
|
+
if (val < node.splitVal) {
|
|
142
|
+
const r = removeFrom(node.left, item, itemBounds, depth + 1)
|
|
143
|
+
node.left = r.node
|
|
144
|
+
if (r.found) recomputeSubtreeBounds(node)
|
|
145
|
+
return { node, found: r.found }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (val > node.splitVal) {
|
|
149
|
+
const r = removeFrom(node.right, item, itemBounds, depth + 1)
|
|
150
|
+
node.right = r.node
|
|
151
|
+
if (r.found) recomputeSubtreeBounds(node)
|
|
152
|
+
return { node, found: r.found }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// val === splitVal, could be on either side due to bulk build ordering
|
|
156
|
+
const r = removeFrom(node.right, item, itemBounds, depth + 1)
|
|
157
|
+
node.right = r.node
|
|
158
|
+
if (r.found) {
|
|
159
|
+
recomputeSubtreeBounds(node)
|
|
160
|
+
return { node, found: true }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const l = removeFrom(node.left, item, itemBounds, depth + 1)
|
|
164
|
+
node.left = l.node
|
|
165
|
+
if (l.found) recomputeSubtreeBounds(node)
|
|
166
|
+
return { node, found: l.found }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function searchTree(node, queryBounds, results) {
|
|
170
|
+
if (!node) return
|
|
171
|
+
if (!intersects(node.subtreeBounds, queryBounds)) return
|
|
172
|
+
|
|
173
|
+
if (intersects(node.entry.bounds, queryBounds)) {
|
|
174
|
+
results.push(node.entry.item)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
searchTree(node.left, queryBounds, results)
|
|
178
|
+
searchTree(node.right, queryBounds, results)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// max-heap for knn (tracks the worst of the k best candidates)
|
|
182
|
+
function maxHeapPush(heap, entry) {
|
|
183
|
+
heap.push(entry)
|
|
184
|
+
let i = heap.length - 1
|
|
185
|
+
while (i > 0) {
|
|
186
|
+
const p = (i - 1) >> 1
|
|
187
|
+
if (heap[p].dist >= heap[i].dist) break
|
|
188
|
+
;[heap[p], heap[i]] = [heap[i], heap[p]]
|
|
189
|
+
i = p
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function maxHeapPop(heap) {
|
|
194
|
+
const top = heap[0]
|
|
195
|
+
const last = heap.pop()
|
|
196
|
+
if (heap.length > 0) {
|
|
197
|
+
heap[0] = last
|
|
198
|
+
let i = 0
|
|
199
|
+
for (;;) {
|
|
200
|
+
let s = i
|
|
201
|
+
const l = 2 * i + 1
|
|
202
|
+
const r = 2 * i + 2
|
|
203
|
+
if (l < heap.length && heap[l].dist > heap[s].dist) s = l
|
|
204
|
+
if (r < heap.length && heap[r].dist > heap[s].dist) s = r
|
|
205
|
+
if (s === i) break
|
|
206
|
+
;[heap[i], heap[s]] = [heap[s], heap[i]]
|
|
207
|
+
i = s
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return top
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function nearestSearch(node, px, py, heap, k) {
|
|
214
|
+
if (!node) return
|
|
215
|
+
|
|
216
|
+
const subtreeDist = distanceToPoint(node.subtreeBounds, px, py)
|
|
217
|
+
if (heap.length >= k && subtreeDist >= heap[0].dist) return
|
|
218
|
+
|
|
219
|
+
const dist = distanceToPoint(node.entry.bounds, px, py)
|
|
220
|
+
if (heap.length < k) {
|
|
221
|
+
maxHeapPush(heap, { dist, item: node.entry.item })
|
|
222
|
+
} else if (dist < heap[0].dist) {
|
|
223
|
+
maxHeapPop(heap)
|
|
224
|
+
maxHeapPush(heap, { dist, item: node.entry.item })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const val = node.splitDim === 0 ? px : py
|
|
228
|
+
const near = val < node.splitVal ? node.left : node.right
|
|
229
|
+
const far = val < node.splitVal ? node.right : node.left
|
|
230
|
+
|
|
231
|
+
nearestSearch(near, px, py, heap, k)
|
|
232
|
+
nearestSearch(far, px, py, heap, k)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function computeBounds(node) {
|
|
236
|
+
if (!node) return null
|
|
237
|
+
return { ...node.subtreeBounds }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Creates a KD-tree spatial index. Optimized for static point sets and nearest-neighbor queries.
|
|
242
|
+
* Supports dynamic inserts and removes, but `load()` produces a balanced tree for best performance.
|
|
243
|
+
*
|
|
244
|
+
* @param {(item: any) => Bounds | object} accessor - Maps items to their bounding boxes or geometries
|
|
245
|
+
* @returns {import('@gridworkjs/core').SpatialIndex & { load: (items: any[]) => void }}
|
|
246
|
+
*/
|
|
247
|
+
export function createKdTree(accessor) {
|
|
248
|
+
if (typeof accessor !== 'function') {
|
|
249
|
+
throw new Error('accessor must be a function')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let root = null
|
|
253
|
+
let size = 0
|
|
254
|
+
let totalBounds = null
|
|
255
|
+
|
|
256
|
+
const index = {
|
|
257
|
+
[SPATIAL_INDEX]: true,
|
|
258
|
+
|
|
259
|
+
get size() { return size },
|
|
260
|
+
|
|
261
|
+
get bounds() { return totalBounds },
|
|
262
|
+
|
|
263
|
+
insert(item) {
|
|
264
|
+
const raw = accessor(item)
|
|
265
|
+
const itemBounds = normalizeBounds(raw)
|
|
266
|
+
validateAccessorBounds(itemBounds)
|
|
267
|
+
|
|
268
|
+
const entry = { item, bounds: itemBounds }
|
|
269
|
+
root = insertInto(root, entry, 0)
|
|
270
|
+
|
|
271
|
+
if (totalBounds === null) {
|
|
272
|
+
totalBounds = { ...itemBounds }
|
|
273
|
+
} else {
|
|
274
|
+
expandBounds(totalBounds, itemBounds)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
size++
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
load(items) {
|
|
281
|
+
root = null
|
|
282
|
+
size = 0
|
|
283
|
+
totalBounds = null
|
|
284
|
+
|
|
285
|
+
if (!items || items.length === 0) return
|
|
286
|
+
|
|
287
|
+
const entries = items.map(item => {
|
|
288
|
+
const raw = accessor(item)
|
|
289
|
+
const itemBounds = normalizeBounds(raw)
|
|
290
|
+
validateAccessorBounds(itemBounds)
|
|
291
|
+
return { item, bounds: itemBounds }
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
root = buildSubtree(entries, 0)
|
|
295
|
+
size = entries.length
|
|
296
|
+
totalBounds = computeBounds(root)
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
remove(item) {
|
|
300
|
+
if (size === 0) return false
|
|
301
|
+
|
|
302
|
+
const raw = accessor(item)
|
|
303
|
+
const itemBounds = normalizeBounds(raw)
|
|
304
|
+
|
|
305
|
+
const result = removeFrom(root, item, itemBounds, 0)
|
|
306
|
+
root = result.node
|
|
307
|
+
|
|
308
|
+
if (result.found) {
|
|
309
|
+
size--
|
|
310
|
+
totalBounds = computeBounds(root)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result.found
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
search(query) {
|
|
317
|
+
if (size === 0) return []
|
|
318
|
+
const queryBounds = normalizeBounds(query)
|
|
319
|
+
const results = []
|
|
320
|
+
searchTree(root, queryBounds, results)
|
|
321
|
+
return results
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
nearest(queryPoint, k = 1) {
|
|
325
|
+
if (size === 0 || k <= 0) return []
|
|
326
|
+
|
|
327
|
+
const px = queryPoint.x
|
|
328
|
+
const py = queryPoint.y
|
|
329
|
+
|
|
330
|
+
const heap = []
|
|
331
|
+
nearestSearch(root, px, py, heap, k)
|
|
332
|
+
|
|
333
|
+
const results = []
|
|
334
|
+
while (heap.length > 0) {
|
|
335
|
+
results.push(maxHeapPop(heap).item)
|
|
336
|
+
}
|
|
337
|
+
results.reverse()
|
|
338
|
+
return results
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
clear() {
|
|
342
|
+
root = null
|
|
343
|
+
size = 0
|
|
344
|
+
totalBounds = null
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return index
|
|
349
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createKdTree } from "./kd.js";
|
package/types/kd.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a KD-tree spatial index. Optimized for static point sets and nearest-neighbor queries.
|
|
3
|
+
* Supports dynamic inserts and removes, but `load()` produces a balanced tree for best performance.
|
|
4
|
+
*
|
|
5
|
+
* @param {(item: any) => Bounds | object} accessor - Maps items to their bounding boxes or geometries
|
|
6
|
+
* @returns {import('@gridworkjs/core').SpatialIndex & { load: (items: any[]) => void }}
|
|
7
|
+
*/
|
|
8
|
+
export function createKdTree(accessor: (item: any) => Bounds | object): any & {
|
|
9
|
+
load: (items: any[]) => void;
|
|
10
|
+
};
|
|
11
|
+
export type Bounds<T> = {
|
|
12
|
+
minX: number;
|
|
13
|
+
minY: number;
|
|
14
|
+
maxX: number;
|
|
15
|
+
maxY: number;
|
|
16
|
+
};
|
|
17
|
+
export type Point<T> = {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
};
|
|
21
|
+
export type Accessor<T> = (item: T) => Bounds;
|