Skip to content

Commit 5a2d8aa

Browse files
committed
more tests
1 parent 23cf90f commit 5a2d8aa

File tree

7 files changed

+107
-76
lines changed

7 files changed

+107
-76
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import colorNames from 'color-names';
1616
import { Octree } from 'ktree';
1717

18-
// simple hex-to-rgb (assuming hex are not in short formats, else see https://unpkg.com/color-tf/hexToRgb.js)
18+
// simple hex-to-rgb (assuming no short formats, else see https://unpkg.com/color-tf/hexToRgb.js)
1919
const hexToRgb = s => [s.slice(-6, -4), s.slice(-4, -2), s.slice(-2)].map(x => parseInt(x, 16));
2020

2121
// we expect an array of {name, [key]} objects, where key is configurable

ktree.js

Lines changed: 65 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -13,49 +13,6 @@ if (!String.prototype.padStart) {
1313
// carterian products
1414
export const cart = (...args) => args.reduce((xs, a) => xs.flatMap(xsi => a.map(ai => [...xsi, ai])), [[]]);
1515

16-
const NS2 = cart([-1, 0, 1], [-1, 0, 1]);
17-
18-
export const neighbors2 = ([x, y]) => {
19-
const xDec = parseInt(x, 2);
20-
const yDec = parseInt(y, 2);
21-
const nodes = [];
22-
const n = x.length, N = 2 ** n;
23-
for (let i = 0; i < NS2.length; i++) {
24-
const X = xDec + NS2[i][0],
25-
Y = yDec + NS2[i][1];
26-
if (X >= 0 && X < N && Y >= 0 && Y < N) {
27-
nodes.push([
28-
X.toString(2).padStart(n, 0),
29-
Y.toString(2).padStart(n, 0)
30-
]);
31-
}
32-
}
33-
return nodes;
34-
};
35-
36-
const NS3 = cart([-1, 0, 1], [-1, 0, 1], [-1, 0, 1]);
37-
38-
export const neighbors3 = ([x, y, z]) => {
39-
const xDec = parseInt(x, 2);
40-
const yDec = parseInt(y, 2);
41-
const zDec = parseInt(z, 2);
42-
const nodes = [];
43-
const n = x.length, N = 2 ** n;
44-
for (let i = 0; i < NS3.length; i++) {
45-
const X = xDec + NS3[i][0],
46-
Y = yDec + NS3[i][1],
47-
Z = zDec + NS3[i][2];
48-
if (X >= 0 && X < N && Y >= 0 && Y < N && Z >= 0 && Z < N) {
49-
nodes.push([
50-
X.toString(2).padStart(n, 0),
51-
Y.toString(2).padStart(n, 0),
52-
Z.toString(2).padStart(n, 0)
53-
]);
54-
}
55-
}
56-
return nodes;
57-
};
58-
5916
const dist2 = (c1, c2) => (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2;
6017
const dist3 = (c1, c2) => (c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2;
6118
const distk = (c1, c2) => c1.map((_, i) => (c1[i] - c2[i]) ** 2).reduce((a, b) => a + b);
@@ -95,20 +52,55 @@ const getCoord2 = (bins, i) => bins[0][i] + bins[1][i];
9552
const getCoord3 = (bins, i) => bins[0][i] + bins[1][i] + bins[2][i];
9653
const getCoordk = (bins, i) => bins.map(s => s[i]).reduce((a, b) => a + b);
9754

55+
// min distance between point coords and the bounding top-left, bottom-right points, with resolution res
56+
const minDist = (coords, tl, br, res) => Math.min(
57+
...coords.map(
58+
(c, j) => Math.min(
59+
c - (tl[j] << res),
60+
(br[j] + 1) << res) - c
61+
)
62+
);
63+
9864

9965
// generate a KTree class for a given k, for k=2, k=3 we try to put inline functions (defined above) for perf
10066
// todo bench if it's really faster
10167
export const ktree = k => {
10268

10369
const KEYS = cart(...Array.from({ length: k }, () => [0, 1])).map(a => a.join('')); // node's children keys
70+
const NS = cart(...Array.from({ length: k }, () => [-1, 0, 1])); // used for getNeighbors
10471

105-
let buildTree = buildTree2;
106-
let getNeighbors = neighbors2;
107-
if (k === 3) {
72+
let buildTree;
73+
let getNeighbors;
74+
if (k === 2) {
75+
buildTree = buildTree2
76+
getNeighbors = ([x, y], N) => {
77+
const nodes = [];
78+
for (let i = 0; i < NS.length; i++) {
79+
const X = x + NS[i][0],
80+
Y = y + NS[i][1];
81+
if (X >= 0 && X < N && Y >= 0 && Y < N) {
82+
nodes.push([X, Y]);
83+
}
84+
}
85+
return nodes;
86+
};
87+
}
88+
else if (k === 3) {
10889
buildTree = buildTree3;
109-
getNeighbors = neighbors3;
90+
getNeighbors = ([x, y, z], N) => {
91+
const nodes = [];
92+
for (let i = 0; i < NS.length; i++) {
93+
const X = x + NS[i][0],
94+
Y = y + NS[i][1],
95+
Z = z + NS[i][2];
96+
if (X >= 0 && X < N && Y >= 0 && Y < N && Z >= 0 && Z < N) {
97+
nodes.push([X, Y, Z]);
98+
}
99+
}
100+
return nodes;
101+
};
110102
}
111-
if (k > 3) {
103+
else {
112104
buildTree = (depth, n = 0) =>
113105
n >= depth
114106
? { n, items: [] }
@@ -117,18 +109,12 @@ export const ktree = k => {
117109
items: [],
118110
...keys.reduce((o, key) => ({ ...o, [key]: buildTree(depth, n + 1) }), {})
119111
};
120-
const NSk = cart(...Array.from({ length: k }, () => [-1, 0, 1]));
121-
getNeighbors = bins => {
122-
const decs = bins.map(b => parseInt(b, 2))
112+
getNeighbors = (decs, N) => {
123113
const nodes = [];
124-
const n = decs[0].length, N = 2 ** n;
125-
for (let i = 0; i < NSk.length; i++) {
126-
const X = xDec + NS3[i][0],
127-
Y = yDec + NS3[i][1],
128-
Z = zDec + NS3[i][2];
129-
const coords = decs.map((dec, j) => dec + NSk[i][j]);
114+
for (let i = 0; i < NS.length; i++) {
115+
const coords = decs.map((dec, j) => dec + NS[i][j]);
130116
if (coords.every(coord => coord >= 0 && coord < N)) {
131-
nodes.push(coords.map(coord => coord.toString(2).padStart(n, 0)));
117+
nodes.push(coords);
132118
}
133119
}
134120
return nodes;
@@ -139,6 +125,7 @@ export const ktree = k => {
139125
const dist = k === 2 ? dist2 : k === 3 ? dist3 : distk;
140126

141127
const getCoord = k === 2 ? getCoord2 : k === 3 ? getCoord3 : getCoordk;
128+
const getCoordV2 = (coords, i) => coords.map(c => c & (1 << i) ? '1' : '0').reduce((a, b) => a + b);
142129

143130
return class KTree {
144131
constructor(items = [], { length = 8, depth = 4, key = 'coords', transform = x => x } = {}) {
@@ -149,7 +136,7 @@ export const ktree = k => {
149136
this.root = buildTree(depth);
150137
this.add(items);
151138
}
152-
coordsToBin(coords) {
139+
coordsToBin(coords) { // todo rename, bin are also coords
153140
return coords.map(x => x.toString(2).padStart(this.len, 0));
154141
}
155142
add(items = []) {
@@ -187,27 +174,37 @@ export const ktree = k => {
187174
}
188175
closest(value) {
189176
const coords = this.transform(value);
190-
const bins = this.coordsToBin(coords);
191-
const node = this.getNodeFromCoords(bins);
177+
const node = this.getNodeFromCoords(this.coordsToBin(coords)); // todo improve
192178
for (let i = node.n; i > 0; i--) {
193-
const parentBins = bins.map(s => s.slice(0, i));
194-
const ns = getNeighbors(parentBins, getCoord(bins, i));
195-
const items = ns
196-
.map(n => this.getNodeFromCoords(n).items)
179+
const res = this.len - i;
180+
const cs = getNeighbors(coords.map(c => c >> res), 1 << i);
181+
const items = cs
182+
.map(c => this.getNodeFromCoordsV2(c, res).items) // todo use flatMap
197183
.reduce((cs, c) => cs.concat(c));
198184
if (items.length) {
199-
return this.closestIn(coords, items);
185+
const item = this.closestIn(coords, items);
186+
// here we must check if the minimal distance from the target to the neighbors square is more than d
187+
// if yes return this value
188+
const minDFromNeighbors = minDist(coords, cs[0], cs[cs.length - 1], res);
189+
if (item.d <= minDFromNeighbors) return item;
200190
}
201191
}
202192
// search in all
203193
const items = KEYS
204-
.map(k => this.root[k].items)
194+
.map(k => this.root[k].items) // todo use flatMap
205195
.reduce((cs, c) => cs.concat(c));
206196
if (items.length) {
207197
return this.closestIn(coords, items);
208198
}
209199
// console.log(`Couldn't find any item`);
210200
}
201+
getNodeFromCoordsV2(coords, resolution = 0) {
202+
let node = this.root;
203+
for (let i = this.len - 1 - resolution; i >= 0 && node[KEYS[0]]; i--) {
204+
node = node[getCoordV2(coords, i)];
205+
}
206+
return node;
207+
}
211208
getNodeFromCoords(coords) {
212209
let node = this.root;
213210
for (let i = 0; i < coords[0].length && node[KEYS[0]]; i++) {
@@ -225,7 +222,7 @@ export const ktree = k => {
225222
current = item;
226223
}
227224
});
228-
return { ...current, d: minD ** .5 };
225+
return { ...current, d: minD ** .5 }; // todo bench vs returning squared distance
229226
}
230227
toJSON(indent = '') {
231228
return JSON.stringify(prettify(this.root, this.key), null, indent);

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "ktree",
3-
"version": "3.0.0",
3+
"version": "3.0.1",
44
"description": "Get closest color",
55
"main": "dist/ktree.cjs.js",
66
"browser": "dist/ktree.umd.js",
77
"module": "dist/ktree.es.js",
88
"scripts": {
9-
"test": "nyc babel-node index.spec",
9+
"test": "nyc babel-node test/index.spec",
1010
"coverage": "nyc report --reporter=lcov > coverage.lcov && codecov",
1111
"build": "NODE_ENV=rollup npx rollup -c",
1212
"prepare": "npm run build"
@@ -41,4 +41,4 @@
4141
"rollup-plugin-babel": "^4.0.0-beta.4"
4242
},
4343
"dependencies": {}
44-
}
44+
}

rollup.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ export default {
1818
},
1919
],
2020
plugins: [
21-
babel({
22-
exclude: 'node_modules/**',
23-
}),
21+
// babel({
22+
// exclude: 'node_modules/**',
23+
// }),
2424
],
2525
};

test/index.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import './quadtree.spec';
2+
import './octree.spec';

index.spec.js renamed to test/octree.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import assert from 'assert';
22
import hexToRgb from 'color-tf/hexToRgb';
33
import colorNameList from 'color-name-list';
44
import colorNames from 'color-names';
5-
import { Octree } from './index';
5+
import { Octree } from '../index';
66

77
console.log('## basic');
88

test/quadtree.spec.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import assert from 'assert';
2+
import { Quadtree } from '../index';
3+
4+
const tf = x => [x.slice(0, 2), x.slice(2, 4)].map(x => parseInt(x, 16));
5+
6+
const data = [ // distance to 1236
7+
'1234',// 2
8+
'1339',// 3.16
9+
'1132',// 4.12
10+
'1330',// 6.08
11+
'1141',// 11.04
12+
'1129',// 13.03
13+
'1327',// 15.03
14+
'2124',// 23.43
15+
'1001',// 53.03
16+
'1595' // 95.04
17+
];
18+
19+
const t = new Quadtree(
20+
data.map(x => ({ name: x })),
21+
{ key: 'name', transform: tf, depth: 1 }
22+
);
23+
24+
let item = t.closest('1236');
25+
26+
while (item) {
27+
t.remove(item.name);
28+
29+
const nextItem = t.closest('1236');
30+
assert(!nextItem || item.d <= nextItem.d)
31+
item = nextItem;
32+
}

0 commit comments

Comments
 (0)