Below is a small R-tree holding 16 rectangles (shown in blue). Click Draw query and drag on the canvas to define a query region (shown in orange). The tree will highlight the bounding boxes it inspects (yellow) and the rectangles it actually returns (green). Click Reset to start over.
<div class="toolbar">
<button id="btnDraw" type="button">Draw query</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
<span id="info" class="info">Click "Draw query", then drag on the canvas.</span>
</div>
<canvas id="canvas" width="560" height="360"></canvas>
<div id="results" class="results"></div>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; }
.toolbar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.info { font-size: .85rem; color: #555; flex: 1; min-width: 160px; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; display: block;
cursor: crosshair; touch-action: none; width: 100%; max-width: 560px; }
.results { margin-top: .5rem; font-size: .85rem; color: #333; min-height: 1.3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// R-Tree demo: a tiny 2-level R-tree of 16 axis-aligned rectangles.
// The user draws a query rectangle; the tree highlights visited bounding boxes
// and returned (intersecting) leaf rectangles.
const W = 560, H = 360;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// โโ Tree data โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 16 leaf rectangles [x, y, w, h] (in canvas coords 0..560 x 0..360)
const LEAVES = [
[30, 20, 60, 40], [110, 30, 50, 35], [30, 90, 55, 45], [100, 80, 45, 50],
[180, 15, 65, 40], [270, 25, 50, 45], [195, 80, 60, 40], [280, 75, 55, 45],
[360, 20, 50, 40], [430, 15, 60, 45], [370, 80, 55, 40], [450, 75, 50, 45],
[50, 200, 55, 45], [130, 210, 60, 40], [55, 270, 50, 40], [140, 275, 55, 45]
];
// Build bounding box of an array of [x,y,w,h]
function bbox(rects) {
let minX=Infinity, minY=Infinity, maxX=-Infinity, maxY=-Infinity;
for (const [x,y,w,h] of rects) {
minX=Math.min(minX,x); minY=Math.min(minY,y);
maxX=Math.max(maxX,x+w); maxY=Math.max(maxY,y+h);
}
return [minX, minY, maxX-minX, maxY-minY];
}
// Group leaves into 4 clusters (the internal nodes of our 2-level tree)
const CLUSTERS = [
{ leaves: [0,1,2,3] },
{ leaves: [4,5,6,7] },
{ leaves: [8,9,10,11] },
{ leaves: [12,13,14,15] }
];
// Compute cluster bounding boxes
for (const c of CLUSTERS) c.bb = bbox(c.leaves.map(i => LEAVES[i]));
// Root bounding box
const ROOT_BB = bbox(CLUSTERS.map(c => [c.bb[0], c.bb[1], c.bb[2], c.bb[3]]));
// โโ Intersection test โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function intersects([ax,ay,aw,ah], [bx,by,bw,bh]) {
return ax < bx+bw && ax+aw > bx && ay < by+bh && ay+ah > by;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw(query, visitedBBs, hitLeaves) {
const scale = canvas.clientWidth / W;
ctx.clearRect(0, 0, W, H);
// Draw all leaf rectangles
for (let i = 0; i < LEAVES.length; i++) {
const [x,y,w,h] = LEAVES[i];
const isHit = hitLeaves && hitLeaves.has(i);
ctx.fillStyle = isHit ? 'rgba(34,197,94,.35)' : 'rgba(59,130,246,.18)';
ctx.strokeStyle = isHit ? '#16a34a' : '#2563eb';
ctx.lineWidth = isHit ? 2 : 1.2;
ctx.beginPath(); ctx.rect(x,y,w,h); ctx.fill(); ctx.stroke();
}
// Draw visited cluster bounding boxes
if (visitedBBs) {
for (const bb of visitedBBs) {
ctx.strokeStyle = '#eab308';
ctx.lineWidth = 1.8;
ctx.setLineDash([5,3]);
ctx.beginPath(); ctx.rect(...bb); ctx.stroke();
ctx.setLineDash([]);
}
}
// Draw query rectangle
if (query) {
ctx.fillStyle = 'rgba(249,115,22,.15)';
ctx.strokeStyle = '#ea580c';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.rect(...query); ctx.fill(); ctx.stroke();
}
}
// โโ R-tree query โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function queryTree(q) {
const visitedBBs = [];
const hitLeaves = new Set();
let inspected = 0;
for (const c of CLUSTERS) {
if (intersects(q, c.bb)) {
visitedBBs.push(c.bb);
inspected++;
for (const idx of c.leaves) {
if (intersects(q, LEAVES[idx])) hitLeaves.add(idx);
}
}
}
return { visitedBBs, hitLeaves, inspected };
}
// โโ Interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let drawing = false, startX = 0, startY = 0, currentQuery = null;
let mode = 'idle'; // 'idle' | 'drawing' | 'done'
function canvasRect() { return canvas.getBoundingClientRect(); }
function toCanvasCoords(clientX, clientY) {
const r = canvasRect();
const scaleX = W / r.width, scaleY = H / r.height;
return [(clientX - r.left) * scaleX, (clientY - r.top) * scaleY];
}
function normRect(x1,y1,x2,y2) {
return [Math.min(x1,x2), Math.min(y1,y2), Math.abs(x2-x1), Math.abs(y2-y1)];
}
function setInfo(msg) { document.getElementById('info').textContent = msg; }
function setResults(msg) { document.getElementById('results').textContent = msg; }
document.getElementById('btnDraw').addEventListener('click', () => {
mode = 'drawing';
currentQuery = null;
draw(null, null, null);
setInfo('Drag on the canvas to draw a query rectangle.');
setResults('');
document.getElementById('btnDraw').disabled = true;
});
document.getElementById('btnReset').addEventListener('click', () => {
mode = 'idle';
currentQuery = null;
draw(null, null, null);
setInfo('Click "Draw query", then drag on the canvas.');
setResults('');
document.getElementById('btnDraw').disabled = false;
});
canvas.addEventListener('mousedown', e => {
if (mode !== 'drawing') return;
drawing = true;
[startX, startY] = toCanvasCoords(e.clientX, e.clientY);
});
canvas.addEventListener('mousemove', e => {
if (!drawing) return;
const [cx, cy] = toCanvasCoords(e.clientX, e.clientY);
const q = normRect(startX, startY, cx, cy);
draw(q, null, null);
});
canvas.addEventListener('mouseup', e => {
if (!drawing) return;
drawing = false;
const [cx, cy] = toCanvasCoords(e.clientX, e.clientY);
const q = normRect(startX, startY, cx, cy);
if (q[2] < 5 || q[3] < 5) { setInfo('Drag a larger rectangle.'); draw(null,null,null); return; }
currentQuery = q;
mode = 'done';
const { visitedBBs, hitLeaves, inspected } = queryTree(q);
draw(q, visitedBBs, hitLeaves);
const skipped = CLUSTERS.length - inspected;
setInfo('Query complete.');
setResults(
fmt('Visited %inspected% of %length% bounding boxes (skipped %skipped%). Found %size% of %length2% rectangles.', { inspected: inspected, length: CLUSTERS.length, skipped: skipped, size: hitLeaves.size, length2: LEAVES.length })
);
document.getElementById('btnDraw').disabled = false;
});
// Touch support
canvas.addEventListener('touchstart', e => { e.preventDefault();
if (mode !== 'drawing') return;
drawing = true;
[startX, startY] = toCanvasCoords(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: false });
canvas.addEventListener('touchmove', e => { e.preventDefault();
if (!drawing) return;
const [cx, cy] = toCanvasCoords(e.touches[0].clientX, e.touches[0].clientY);
draw(normRect(startX, startY, cx, cy), null, null);
}, { passive: false });
canvas.addEventListener('touchend', e => { e.preventDefault();
if (!drawing) return;
drawing = false;
const t = e.changedTouches[0];
const [cx, cy] = toCanvasCoords(t.clientX, t.clientY);
const q = normRect(startX, startY, cx, cy);
if (q[2] < 5 || q[3] < 5) return;
currentQuery = q; mode = 'done';
const { visitedBBs, hitLeaves, inspected } = queryTree(q);
draw(q, visitedBBs, hitLeaves);
const skipped = CLUSTERS.length - inspected;
setResults(fmt('Visited %inspected% of %length% bounding boxes (skipped %skipped%). Found %size% of %length2% rectangles.', { inspected: inspected, length: CLUSTERS.length, skipped: skipped, size: hitLeaves.size, length2: LEAVES.length }));
document.getElementById('btnDraw').disabled = false;
}, { passive: false });
// Initial render
draw(null, null, null);
setInfo('Click "Draw query", then drag on the canvas.');
// โโ Node-verifiable logic (run in browser console or node) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Cluster bboxes: 0=[30,20,130,115], 1=[180,15,155,105], 2=[360,15,140,105], 3=[50,200,145,120]
// Verify: a query covering only cluster 0's area visits exactly 1 bounding box / 4 leaves.
const testQ = [20, 15, 140, 130]; // right=160 < 180 (cluster 1 starts at 180)
const testR = queryTree(testQ);
console.assert(testR.inspected === 1, fmt('Should visit 1 cluster, got %inspected%', { inspected: testR.inspected }));
console.assert(testR.hitLeaves.size === 4, fmt('Should hit 4 leaves, got %size%', { size: testR.hitLeaves.size }));
// A query covering nothing should visit 0 bounding boxes
const emptyQ = [400, 280, 50, 50]; // bottom-right corner, no leaves there
const emptyR = queryTree(emptyQ);
console.assert(emptyR.hitLeaves.size === 0, 'Empty query should hit 0 leaves');
console.log('R-tree demo assertions passed.');
<div class="toolbar">
<button id="btnDraw" type="button">Draw query</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
<span id="info" class="info">Click "Draw query", then drag on the canvas.</span>
</div>
<canvas id="canvas" width="560" height="360"></canvas>
<div id="results" class="results"></div>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; }
.toolbar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.info { font-size: .85rem; color: #555; flex: 1; min-width: 160px; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; display: block;
cursor: crosshair; touch-action: none; width: 100%; max-width: 560px; }
.results { margin-top: .5rem; font-size: .85rem; color: #333; min-height: 1.3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// R-Tree demo: a tiny 2-level R-tree of 16 axis-aligned rectangles.
// The user draws a query rectangle; the tree highlights visited bounding boxes
// and returned (intersecting) leaf rectangles.
const W = 560, H = 360;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// โโ Tree data โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 16 leaf rectangles [x, y, w, h] (in canvas coords 0..560 x 0..360)
const LEAVES = [
[30, 20, 60, 40], [110, 30, 50, 35], [30, 90, 55, 45], [100, 80, 45, 50],
[180, 15, 65, 40], [270, 25, 50, 45], [195, 80, 60, 40], [280, 75, 55, 45],
[360, 20, 50, 40], [430, 15, 60, 45], [370, 80, 55, 40], [450, 75, 50, 45],
[50, 200, 55, 45], [130, 210, 60, 40], [55, 270, 50, 40], [140, 275, 55, 45]
];
// Build bounding box of an array of [x,y,w,h]
function bbox(rects) {
let minX=Infinity, minY=Infinity, maxX=-Infinity, maxY=-Infinity;
for (const [x,y,w,h] of rects) {
minX=Math.min(minX,x); minY=Math.min(minY,y);
maxX=Math.max(maxX,x+w); maxY=Math.max(maxY,y+h);
}
return [minX, minY, maxX-minX, maxY-minY];
}
// Group leaves into 4 clusters (the internal nodes of our 2-level tree)
const CLUSTERS = [
{ leaves: [0,1,2,3] },
{ leaves: [4,5,6,7] },
{ leaves: [8,9,10,11] },
{ leaves: [12,13,14,15] }
];
// Compute cluster bounding boxes
for (const c of CLUSTERS) c.bb = bbox(c.leaves.map(i => LEAVES[i]));
// Root bounding box
const ROOT_BB = bbox(CLUSTERS.map(c => [c.bb[0], c.bb[1], c.bb[2], c.bb[3]]));
// โโ Intersection test โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function intersects([ax,ay,aw,ah], [bx,by,bw,bh]) {
return ax < bx+bw && ax+aw > bx && ay < by+bh && ay+ah > by;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw(query, visitedBBs, hitLeaves) {
const scale = canvas.clientWidth / W;
ctx.clearRect(0, 0, W, H);
// Draw all leaf rectangles
for (let i = 0; i < LEAVES.length; i++) {
const [x,y,w,h] = LEAVES[i];
const isHit = hitLeaves && hitLeaves.has(i);
ctx.fillStyle = isHit ? 'rgba(34,197,94,.35)' : 'rgba(59,130,246,.18)';
ctx.strokeStyle = isHit ? '#16a34a' : '#2563eb';
ctx.lineWidth = isHit ? 2 : 1.2;
ctx.beginPath(); ctx.rect(x,y,w,h); ctx.fill(); ctx.stroke();
}
// Draw visited cluster bounding boxes
if (visitedBBs) {
for (const bb of visitedBBs) {
ctx.strokeStyle = '#eab308';
ctx.lineWidth = 1.8;
ctx.setLineDash([5,3]);
ctx.beginPath(); ctx.rect(...bb); ctx.stroke();
ctx.setLineDash([]);
}
}
// Draw query rectangle
if (query) {
ctx.fillStyle = 'rgba(249,115,22,.15)';
ctx.strokeStyle = '#ea580c';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.rect(...query); ctx.fill(); ctx.stroke();
}
}
// โโ R-tree query โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function queryTree(q) {
const visitedBBs = [];
const hitLeaves = new Set();
let inspected = 0;
for (const c of CLUSTERS) {
if (intersects(q, c.bb)) {
visitedBBs.push(c.bb);
inspected++;
for (const idx of c.leaves) {
if (intersects(q, LEAVES[idx])) hitLeaves.add(idx);
}
}
}
return { visitedBBs, hitLeaves, inspected };
}
// โโ Interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let drawing = false, startX = 0, startY = 0, currentQuery = null;
let mode = 'idle'; // 'idle' | 'drawing' | 'done'
function canvasRect() { return canvas.getBoundingClientRect(); }
function toCanvasCoords(clientX, clientY) {
const r = canvasRect();
const scaleX = W / r.width, scaleY = H / r.height;
return [(clientX - r.left) * scaleX, (clientY - r.top) * scaleY];
}
function normRect(x1,y1,x2,y2) {
return [Math.min(x1,x2), Math.min(y1,y2), Math.abs(x2-x1), Math.abs(y2-y1)];
}
function setInfo(msg) { document.getElementById('info').textContent = msg; }
function setResults(msg) { document.getElementById('results').textContent = msg; }
document.getElementById('btnDraw').addEventListener('click', () => {
mode = 'drawing';
currentQuery = null;
draw(null, null, null);
setInfo('Drag on the canvas to draw a query rectangle.');
setResults('');
document.getElementById('btnDraw').disabled = true;
});
document.getElementById('btnReset').addEventListener('click', () => {
mode = 'idle';
currentQuery = null;
draw(null, null, null);
setInfo('Click "Draw query", then drag on the canvas.');
setResults('');
document.getElementById('btnDraw').disabled = false;
});
canvas.addEventListener('mousedown', e => {
if (mode !== 'drawing') return;
drawing = true;
[startX, startY] = toCanvasCoords(e.clientX, e.clientY);
});
canvas.addEventListener('mousemove', e => {
if (!drawing) return;
const [cx, cy] = toCanvasCoords(e.clientX, e.clientY);
const q = normRect(startX, startY, cx, cy);
draw(q, null, null);
});
canvas.addEventListener('mouseup', e => {
if (!drawing) return;
drawing = false;
const [cx, cy] = toCanvasCoords(e.clientX, e.clientY);
const q = normRect(startX, startY, cx, cy);
if (q[2] < 5 || q[3] < 5) { setInfo('Drag a larger rectangle.'); draw(null,null,null); return; }
currentQuery = q;
mode = 'done';
const { visitedBBs, hitLeaves, inspected } = queryTree(q);
draw(q, visitedBBs, hitLeaves);
const skipped = CLUSTERS.length - inspected;
setInfo('Query complete.');
setResults(
fmt('Visited %inspected% of %length% bounding boxes (skipped %skipped%). Found %size% of %length2% rectangles.', { inspected: inspected, length: CLUSTERS.length, skipped: skipped, size: hitLeaves.size, length2: LEAVES.length })
);
document.getElementById('btnDraw').disabled = false;
});
// Touch support
canvas.addEventListener('touchstart', e => { e.preventDefault();
if (mode !== 'drawing') return;
drawing = true;
[startX, startY] = toCanvasCoords(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: false });
canvas.addEventListener('touchmove', e => { e.preventDefault();
if (!drawing) return;
const [cx, cy] = toCanvasCoords(e.touches[0].clientX, e.touches[0].clientY);
draw(normRect(startX, startY, cx, cy), null, null);
}, { passive: false });
canvas.addEventListener('touchend', e => { e.preventDefault();
if (!drawing) return;
drawing = false;
const t = e.changedTouches[0];
const [cx, cy] = toCanvasCoords(t.clientX, t.clientY);
const q = normRect(startX, startY, cx, cy);
if (q[2] < 5 || q[3] < 5) return;
currentQuery = q; mode = 'done';
const { visitedBBs, hitLeaves, inspected } = queryTree(q);
draw(q, visitedBBs, hitLeaves);
const skipped = CLUSTERS.length - inspected;
setResults(fmt('Visited %inspected% of %length% bounding boxes (skipped %skipped%). Found %size% of %length2% rectangles.', { inspected: inspected, length: CLUSTERS.length, skipped: skipped, size: hitLeaves.size, length2: LEAVES.length }));
document.getElementById('btnDraw').disabled = false;
}, { passive: false });
// Initial render
draw(null, null, null);
setInfo('Click "Draw query", then drag on the canvas.');
// โโ Node-verifiable logic (run in browser console or node) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Cluster bboxes: 0=[30,20,130,115], 1=[180,15,155,105], 2=[360,15,140,105], 3=[50,200,145,120]
// Verify: a query covering only cluster 0's area visits exactly 1 bounding box / 4 leaves.
const testQ = [20, 15, 140, 130]; // right=160 < 180 (cluster 1 starts at 180)
const testR = queryTree(testQ);
console.assert(testR.inspected === 1, fmt('Should visit 1 cluster, got %inspected%', { inspected: testR.inspected }));
console.assert(testR.hitLeaves.size === 4, fmt('Should hit 4 leaves, got %size%', { size: testR.hitLeaves.size }));
// A query covering nothing should visit 0 bounding boxes
const emptyQ = [400, 280, 50, 50]; // bottom-right corner, no leaves there
const emptyR = queryTree(emptyQ);
console.assert(emptyR.hitLeaves.size === 0, 'Empty query should hit 0 leaves');
console.log('R-tree demo assertions passed.');
Notice how the tree skips entire clusters the moment their bounding box misses the query region. With 16 rectangles the saving is modest; with millions of shapes the pruning makes the difference between milliseconds and minutes.
Comments
Loading comments...