[
  {
    "path": ".github/workflow/node.yml",
    "content": "name: Node\non: [push, pull_request]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Setup Node\n      uses: actions/setup-node@v4\n      with:\n        node-version: 20\n\n    - name: Install dependencies\n      run: npm ci\n\n    - name: Build the bundle\n      run: npm run build\n\n    - name: Run tests\n      run: npm test\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\nrbush-knn.js\nrbush-knn.min.js\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2016, Vladimir Agafonkin\n\nPermission to use, copy, modify, and/or distribute this software for any purpose\nwith or without fee is hereby granted, provided that the above copyright notice\nand this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\nOF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\nTORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF\nTHIS SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "## rbush-knn\n\n_k_-nearest neighbors search for [RBush](https://github.com/mourner/rbush).\nImplements a simple depth-first kNN search algorithm using a priority queue.\n\n```js\nimport RBush from 'rbush';\nimport knn from 'rbush-knn';\n\nconst tree = new RBush(); // create RBush tree\ntree.load(data); // bulk insert\n\nconst neighbors = knn(tree, 40, 40, 10); // return 10 nearest items around point [40, 40]\n```\n\nYou can optionally pass a filter function to find k neighbors that satisfy a certain condition:\n\n```js\nconst neighbors = knn(tree, 40, 40, 10, function (item) {\n    return item.foo === 'bar';\n});\n```\n\n### API\n\n**knn(tree, x, y, [k, filterFn, maxDistance])**\n\n- `tree`: an RBush tree\n- `x`, `y`: query coordinates\n- `k`: number of neighbors to search for (`Infinity` by default)\n- `filterFn`: optional filter function; `k` nearest items where `filterFn(item) === true` will be returned.\n- `maxDistance` (optional): maximum distance between neighbors and the query coordinates (`Infinity` by default)\n"
  },
  {
    "path": "bench.js",
    "content": "import RBush from 'rbush';\nimport knn from './index.js';\n\nconst N = 200000,\n    M = 20000,\n    K = 5;\n\nconst points = [];\nconst queries = [];\nfor (let i = 0; i < N; i++) {\n    points.push(randPoint());\n    queries.push(randPoint());\n}\n\nconsole.time(`load ${N} points`);\nconst tree = new RBush().load(points);\nconsole.timeEnd(`load ${N} points`);\n\nconsole.time(`knn query ${K} neighbors x ${M}`);\nfor (let i = 0; i < M; i++) {\n    knn(tree, queries[i].minX, queries[i].minY, K);\n}\nconsole.timeEnd(`knn query ${K} neighbors x ${M}`);\n\n\nconsole.time(`bbox query x ${  M}`);\nfor (let i = 0; i < M; i++) {\n    tree.search(queries[i]);\n}\nconsole.timeEnd(`bbox query x ${M}`);\n\nfunction randPoint() {\n    const x = Math.floor(Math.random() * 100000),\n        y = Math.floor(Math.random() * 100000);\n    return {\n        minX: x,\n        minY: y,\n        maxX: x,\n        maxY: y\n    };\n}\n"
  },
  {
    "path": "eslint.config.js",
    "content": "export {default} from 'eslint-config-mourner';\n"
  },
  {
    "path": "index.js",
    "content": "import Queue from 'tinyqueue';\n\nexport default function knn(tree, x, y, n, predicate, maxDistance) {\n    let node = tree.data;\n    const result = [];\n    const toBBox = tree.toBBox;\n\n    const queue = new Queue(undefined, compareDist);\n\n    while (node) {\n        for (let i = 0; i < node.children.length; i++) {\n            const child = node.children[i];\n            const dist = boxDist(x, y, node.leaf ? toBBox(child) : child);\n            if (!maxDistance || dist <= maxDistance * maxDistance) {\n                queue.push({\n                    node: child,\n                    isItem: node.leaf,\n                    dist\n                });\n            }\n        }\n\n        while (queue.length && queue.peek().isItem) {\n            const candidate = queue.pop().node;\n            if (!predicate || predicate(candidate))\n                result.push(candidate);\n            if (n && result.length === n) return result;\n        }\n\n        node = queue.pop();\n        if (node) node = node.node;\n    }\n\n    return result;\n}\n\nfunction compareDist(a, b) {\n    return a.dist - b.dist;\n}\n\nfunction boxDist(x, y, box) {\n    const dx = axisDist(x, box.minX, box.maxX),\n        dy = axisDist(y, box.minY, box.maxY);\n    return dx * dx + dy * dy;\n}\n\nfunction axisDist(k, min, max) {\n    return k < min ? min - k : k <= max ? 0 : k - max;\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"rbush-knn\",\n  \"version\": \"4.0.0\",\n  \"description\": \"k-neareset neighbors search for RBush\",\n  \"main\": \"rbush-knn.js\",\n  \"module\": \"index.js\",\n  \"browser\": \"rbush-knn.min.js\",\n  \"jsdelivr\": \"rbush-knn.min.js\",\n  \"unpkg\": \"rbush-knn.min.js\",\n  \"exports\": \"./index.js\",\n  \"scripts\": {\n    \"bench\": \"node bench.js\",\n    \"lint\": \"eslint index.js test.js bench.js\",\n    \"pretest\": \"npm run lint\",\n    \"test\": \"node --test\",\n    \"build\": \"rollup -c\",\n    \"prepare\": \"npm run build\"\n  },\n  \"keywords\": [\n    \"rbush\",\n    \"knn\",\n    \"k-nearest neighbors\",\n    \"data structure\",\n    \"query\"\n  ],\n  \"author\": \"Vladimir Agafonkin\",\n  \"license\": \"ISC\",\n  \"type\": \"module\",\n  \"devDependencies\": {\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"@rollup/plugin-terser\": \"^0.4.4\",\n    \"eslint\": \"^9.6.0\",\n    \"eslint-config-mourner\": \"^4.0.1\",\n    \"rbush\": \"^4.0.0\",\n    \"rollup\": \"^4.18.0\"\n  },\n  \"dependencies\": {\n    \"tinyqueue\": \"^2.0.3\"\n  },\n  \"files\": [\n    \"index.js\",\n    \"rbush-knn.js\",\n    \"rbush-knn.min.js\"\n  ],\n  \"repository\": \"mourner/rbush-knn\"\n}\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import terser from '@rollup/plugin-terser';\nimport resolve from '@rollup/plugin-node-resolve';\n\nconst output = (file, plugins) => ({\n    input: 'index.js',\n    output: {\n        name: 'rbush-knn',\n        format: 'umd',\n        file\n    },\n    plugins\n});\n\nexport default [\n    output('rbush-knn.js', [resolve()]),\n    output('rbush-knn.min.js', [resolve(), terser()])\n];\n"
  },
  {
    "path": "test.js",
    "content": "import RBush from 'rbush';\nimport test from 'node:test';\nimport assert from 'node:assert/strict';\n\nimport knn from './index.js';\n\nfunction rbush() {\n    return new RBush();\n}\n\n/*eslint  @stylistic/js/comma-spacing: 0 */\n\nfunction arrToBox(arr) {\n    return {\n        minX: arr[0],\n        minY: arr[1],\n        maxX: arr[2],\n        maxY: arr[3]\n    };\n}\n\nconst data = [[87,55,87,56],[38,13,39,16],[7,47,8,47],[89,9,91,12],[4,58,5,60],[0,11,1,12],[0,5,0,6],[69,78,73,78],\n    [56,77,57,81],[23,7,24,9],[68,24,70,26],[31,47,33,50],[11,13,14,15],[1,80,1,80],[72,90,72,91],[59,79,61,83],\n    [98,77,101,77],[11,55,14,56],[98,4,100,6],[21,54,23,58],[44,74,48,74],[70,57,70,61],[32,9,33,12],[43,87,44,91],\n    [38,60,38,60],[62,48,66,50],[16,87,19,91],[5,98,9,99],[9,89,10,90],[89,2,92,6],[41,95,45,98],[57,36,61,40],\n    [50,1,52,1],[93,87,96,88],[29,42,33,42],[34,43,36,44],[41,64,42,65],[87,3,88,4],[56,50,56,52],[32,13,35,15],\n    [3,8,5,11],[16,33,18,33],[35,39,38,40],[74,54,78,56],[92,87,95,90],[12,97,16,98],[76,39,78,40],[16,93,18,95],\n    [62,40,64,42],[71,87,71,88],[60,85,63,86],[39,52,39,56],[15,18,19,18],[91,62,94,63],[10,16,10,18],[5,86,8,87],\n    [85,85,88,86],[44,84,44,88],[3,94,3,97],[79,74,81,78],[21,63,24,66],[16,22,16,22],[68,97,72,97],[39,65,42,65],\n    [51,68,52,69],[61,38,61,42],[31,65,31,65],[16,6,19,6],[66,39,66,41],[57,32,59,35],[54,80,58,84],[5,67,7,71],\n    [49,96,51,98],[29,45,31,47],[31,72,33,74],[94,25,95,26],[14,7,18,8],[29,0,31,1],[48,38,48,40],[34,29,34,32],\n    [99,21,100,25],[79,3,79,4],[87,1,87,5],[9,77,9,81],[23,25,25,29],[83,48,86,51],[79,94,79,95],[33,95,33,99],\n    [1,14,1,14],[33,77,34,77],[94,56,98,59],[75,25,78,26],[17,73,20,74],[11,3,12,4],[45,12,47,12],[38,39,39,39],\n    [99,3,103,5],[41,92,44,96],[79,40,79,41],[29,2,29,4]].map(arrToBox);\n\ntest('finds n neighbours', () => {\n    const tree = rbush().load(data);\n    const result = knn(tree, 40, 40, 10);\n    assert.deepEqual(result, [[38,39,39,39],[35,39,38,40],[34,43,36,44],[29,42,33,42],[48,38,48,40],[31,47,33,50],[34,29,34,32],\n        [29,45,31,47],[39,52,39,56],[57,36,61,40]].map(arrToBox));\n});\n\ntest('does not throw if requesting too many items', () => {\n    const tree = rbush().load(data);\n    assert.doesNotThrow(() => {\n        const result = knn(tree, 40, 40, 1000);\n        assert.equal(result.length, data.length);\n    });\n});\n\ntest('finds all neighbors for maxDistance', () => {\n    const tree = rbush().load(data);\n    const result = knn(tree, 40, 40, 0, null, 10);\n    assert.deepEqual(result, [[38,39,39,39],[35,39,38,40],[34,43,36,44],[29,42,33,42],[48,38,48,40],[31,47,33,50],[34,29,34,32]].map(arrToBox));\n});\n\ntest('finds n neighbors for maxDistance', () => {\n    const tree = rbush().load(data);\n    const result = knn(tree, 40, 40, 1, null, 10);\n    assert.deepEqual(result, [[38,39,39,39]].map(arrToBox));\n});\n\ntest('does not throw if requesting too many items for maxDistance', () => {\n    const tree = rbush().load(data);\n    assert.doesNotThrow(() => {\n        const result = knn(tree, 40, 40, 1000, null, 10);\n        assert.deepEqual(result, [[38,39,39,39],[35,39,38,40],[34,43,36,44],[29,42,33,42],[48,38,48,40],[31,47,33,50],[34,29,34,32]].map(arrToBox));\n    });\n});\n\nconst pythData = [[0,0,0,0],[9,9,9,9],[12,12,12,12],[13,14,19,11]].map(arrToBox);\n\ntest('verify maxDistance excludes items too far away, in order to adhere to pythagoras theorem a^2+b^2=c^2', () => {\n    const tree = rbush().load(pythData);\n    // sqrt(9^2+9^2)~=12.727\n    const result = knn(tree, 0, 0, 1000, null, 12.6);\n    assert.deepEqual(result, [[0,0,0,0]].map(arrToBox));\n});\n\ntest('verify maxDistance includes all items within range, in order to adhere to pythagoras theorem a^2+b^2=c^2', () => {\n    const tree = rbush().load(pythData);\n    // sqrt(9^2+9^2)~=12.727\n    const result = knn(tree, 0, 0, 1000, null, 12.8);\n    assert.deepEqual(result, [[0,0,0,0],[9,9,9,9]].map(arrToBox));\n});\n\nconst richData = [[1,2,1,2],[3,3,3,3],[5,5,5,5],[4,2,4,2],[2,4,2,4],[5,3,5,3]].map((a, i) => {\n    const item = arrToBox(a);\n    item.version = i + 1;\n    return item;\n});\n\ntest('find n neighbours that do satisfy a given predicate', () => {\n    const tree = rbush().load(richData);\n    const result = knn(tree, 2, 4, 1, item => item.version < 5);\n    if (result.length === 1) {\n        const item = result[0];\n        if (item.minX === 3 && item.minY === 3 && item.maxX === 3 && item.maxY === 3 && item.version === 2) {\n            // OK\n        } else {\n            assert.fail(`Could not find the correct item, found ${  JSON.stringify(item)}`);\n        }\n    } else {\n        assert.fail('Could not find the correct item');\n    }\n});\n"
  }
]