|
|
|
@ -19,23 +19,40 @@ class MTree {
|
|
|
|
* @param {number[]} point - The point to insert
|
|
|
|
* @param {number[]} point - The point to insert
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
insert(point) {
|
|
|
|
insert(point) {
|
|
|
|
const node = this.findLeafNode(this.root, point);
|
|
|
|
// Find the leaf node that should hold the given point
|
|
|
|
if (!node) {
|
|
|
|
const leafNode = this.findLeafNode(this.root, point);
|
|
|
|
|
|
|
|
if (!leafNode) {
|
|
|
|
throw new Error('No leaf node found for insertion');
|
|
|
|
throw new Error('No leaf node found for insertion');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Create a new ground entry from the point
|
|
|
|
const groundEntry = new GroundEntry(point);
|
|
|
|
const groundEntry = new GroundEntry(point);
|
|
|
|
node.entries.push(groundEntry);
|
|
|
|
|
|
|
|
console.log(node.entries.length, this.capacity);
|
|
|
|
// Add the ground entry to the leaf node's entries
|
|
|
|
if (node.entries.length > this.capacity) {
|
|
|
|
leafNode.entries.push(groundEntry);
|
|
|
|
this.splitNode(node);
|
|
|
|
|
|
|
|
|
|
|
|
// Update the centroid and radius of the leaf node's routing entry
|
|
|
|
|
|
|
|
if (leafNode.parent !== null) {
|
|
|
|
|
|
|
|
const leafEntry = leafNode.parent.entries.find(entry => entry.node === leafNode);
|
|
|
|
|
|
|
|
if (leafEntry) {
|
|
|
|
|
|
|
|
leafEntry.updateCentroid();
|
|
|
|
|
|
|
|
leafEntry.updateRadius();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update radius for each routing entry from leaf to root
|
|
|
|
// If the leaf node now has more than the capacity amount of entries, split it
|
|
|
|
let currentNode = node;
|
|
|
|
if (leafNode.entries.length > this.capacity) {
|
|
|
|
while (currentNode.parent) {
|
|
|
|
this.splitNode(leafNode);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//console.log(`found leaf node: ${leafNode.id}`);
|
|
|
|
|
|
|
|
// Update the routing entries of ancestors of the leaf node
|
|
|
|
|
|
|
|
let currentNode = leafNode;
|
|
|
|
|
|
|
|
while (currentNode.parent !== null) {
|
|
|
|
const routingEntry = currentNode.parent.entries.find(entry => entry.node === currentNode);
|
|
|
|
const routingEntry = currentNode.parent.entries.find(entry => entry.node === currentNode);
|
|
|
|
if (routingEntry) {
|
|
|
|
if (routingEntry) {
|
|
|
|
|
|
|
|
//console.log(`updating routing entry: ${routingEntry.node.id}`);
|
|
|
|
|
|
|
|
routingEntry.updateCentroid();
|
|
|
|
routingEntry.updateRadius();
|
|
|
|
routingEntry.updateRadius();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
currentNode = currentNode.parent;
|
|
|
|
currentNode = currentNode.parent;
|
|
|
|
@ -54,9 +71,11 @@ class MTree {
|
|
|
|
throw new Error('Node is undefined');
|
|
|
|
throw new Error('Node is undefined');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (node.isLeaf) {
|
|
|
|
if (node.isLeaf) {
|
|
|
|
|
|
|
|
//console.log("found leaf node");
|
|
|
|
return node;
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const closestEntry = this.findClosestEntry(node.entries, point);
|
|
|
|
const closestEntry = this.findClosestEntry(node.entries, point);
|
|
|
|
|
|
|
|
//console.log(`closest node: ${closestEntry.node.id}, distance: ${this.distanceFunction(point, closestEntry.point)}`);
|
|
|
|
if (!closestEntry) {
|
|
|
|
if (!closestEntry) {
|
|
|
|
return node;
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -83,57 +102,6 @@ class MTree {
|
|
|
|
return closestEntry;
|
|
|
|
return closestEntry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Splits the given node into two nodes when the number of entries exceeds capacity.
|
|
|
|
|
|
|
|
* The original node retains half of the entries, and a new node is created with the other half.
|
|
|
|
|
|
|
|
* The new node is linked to the original node as a child by calculating and using the centroid
|
|
|
|
|
|
|
|
* as the connecting entry point.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* @param {Node} node - The node to be split.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
splitRoot(node) {
|
|
|
|
|
|
|
|
if (!node) {
|
|
|
|
|
|
|
|
throw new Error('Node is undefined');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const entries = node.entries;
|
|
|
|
|
|
|
|
const halfSize = Math.floor(entries.length / 2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let minRadius = Infinity;
|
|
|
|
|
|
|
|
let bestSplit = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Loop through all combinations of entries that contain half of the total entries
|
|
|
|
|
|
|
|
const combinations = this.getCombinations(entries, halfSize);
|
|
|
|
|
|
|
|
// console.log(combinations.map(combination => combination.map(entry => entry.point)), "size: " + combinations.length);
|
|
|
|
|
|
|
|
for (const combination of combinations) {
|
|
|
|
|
|
|
|
const leftEntries = combination;
|
|
|
|
|
|
|
|
const rightEntries = entries.filter(entry => !leftEntries.includes(entry));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the radius for each group
|
|
|
|
|
|
|
|
const leftRadius = this.calculateRadius(leftEntries);
|
|
|
|
|
|
|
|
const rightRadius = this.calculateRadius(rightEntries);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the total radius for this split
|
|
|
|
|
|
|
|
const totalRadius = Math.max(leftRadius, rightRadius);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update the best split if this one has a smaller radius
|
|
|
|
|
|
|
|
if (totalRadius < minRadius) {
|
|
|
|
|
|
|
|
minRadius = totalRadius;
|
|
|
|
|
|
|
|
bestSplit = { leftEntries, rightEntries };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const leftNode = new Node(bestSplit.leftEntries, node.isLeaf);
|
|
|
|
|
|
|
|
const rightNode = new Node(bestSplit.rightEntries, node.isLeaf);
|
|
|
|
|
|
|
|
leftNode.parent = node; // Set parent of leftNode
|
|
|
|
|
|
|
|
rightNode.parent = node; // Set parent of rightNode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const rightEntry = new RoutingEntry(rightNode, this);
|
|
|
|
|
|
|
|
const leftEntry = new RoutingEntry(leftNode, this);
|
|
|
|
|
|
|
|
node.entries = [leftEntry, rightEntry];
|
|
|
|
|
|
|
|
node.isLeaf = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Calculates the radius of the given entries.
|
|
|
|
* Calculates the radius of the given entries.
|
|
|
|
* The radius is the maximum distance from the centroid of the entries to any point in the entries.
|
|
|
|
* The radius is the maximum distance from the centroid of the entries to any point in the entries.
|
|
|
|
@ -145,7 +113,7 @@ class MTree {
|
|
|
|
// Calculate the centroid of the entries
|
|
|
|
// Calculate the centroid of the entries
|
|
|
|
const sum = entries.reduce((acc, entry) => {
|
|
|
|
const sum = entries.reduce((acc, entry) => {
|
|
|
|
return acc.map((val, i) => val + entry.point[i]);
|
|
|
|
return acc.map((val, i) => val + entry.point[i]);
|
|
|
|
}, new Array(entries[0].point.length).fill(0));
|
|
|
|
}, new Array(this.dimension).fill(0));
|
|
|
|
const centroidPoint = sum.map(val => val / entries.length);
|
|
|
|
const centroidPoint = sum.map(val => val / entries.length);
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the maximum distance from the centroid to any point in the entries
|
|
|
|
// Calculate the maximum distance from the centroid to any point in the entries
|
|
|
|
@ -154,18 +122,20 @@ class MTree {
|
|
|
|
return maxDistance;
|
|
|
|
return maxDistance;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Splits a node into two nodes by finding the best split that minimizes the total radius of the two nodes.
|
|
|
|
|
|
|
|
* @param {Node} node - The node to split
|
|
|
|
|
|
|
|
*/
|
|
|
|
splitNode(node) {
|
|
|
|
splitNode(node) {
|
|
|
|
const entries = node.entries;
|
|
|
|
const entries = node.entries;
|
|
|
|
const halfSize = Math.floor(entries.length / 2);
|
|
|
|
const halfSize = Math.floor(entries.length / 2);
|
|
|
|
|
|
|
|
|
|
|
|
let minRadius = Infinity;
|
|
|
|
let minTotalRadius = Infinity;
|
|
|
|
let bestSplit = null;
|
|
|
|
let bestSplit = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Loop through all combinations of entries that contain half of the total entries
|
|
|
|
// Loop through all combinations of entries that contain half of the total entries
|
|
|
|
const combinations = this.getCombinations(entries, halfSize);
|
|
|
|
const entryCombinations = this.getCombinations(entries, halfSize);
|
|
|
|
// console.log(combinations.map(combination => combination.map(entry => entry.point)), "size: " + combinations.length);
|
|
|
|
for (const leftEntries of entryCombinations) {
|
|
|
|
for (const combination of combinations) {
|
|
|
|
|
|
|
|
const leftEntries = combination;
|
|
|
|
|
|
|
|
const rightEntries = entries.filter(entry => !leftEntries.includes(entry));
|
|
|
|
const rightEntries = entries.filter(entry => !leftEntries.includes(entry));
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the radius for each group
|
|
|
|
// Calculate the radius for each group
|
|
|
|
@ -173,31 +143,53 @@ class MTree {
|
|
|
|
const rightRadius = this.calculateRadius(rightEntries);
|
|
|
|
const rightRadius = this.calculateRadius(rightEntries);
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the total radius for this split
|
|
|
|
// Calculate the total radius for this split
|
|
|
|
const totalRadius = Math.max(leftRadius, rightRadius);
|
|
|
|
//const totalRadius = Math.max(leftRadius, rightRadius);
|
|
|
|
|
|
|
|
const totalRadius = leftRadius + rightRadius;
|
|
|
|
|
|
|
|
|
|
|
|
// Update the best split if this one has a smaller radius
|
|
|
|
// Update the best split if this one has a smaller radius
|
|
|
|
if (totalRadius < minRadius) {
|
|
|
|
if (totalRadius < minTotalRadius) {
|
|
|
|
minRadius = totalRadius;
|
|
|
|
minTotalRadius = totalRadius;
|
|
|
|
bestSplit = { leftEntries, rightEntries };
|
|
|
|
bestSplit = { leftEntries, rightEntries };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const newNode = new Node(bestSplit.rightEntries, node.isLeaf, node.parent);
|
|
|
|
if (node.parent !== null) {
|
|
|
|
|
|
|
|
|
|
|
|
if (node.parent) {
|
|
|
|
|
|
|
|
// If the node is not a root node, split it into two nodes
|
|
|
|
// If the node is not a root node, split it into two nodes
|
|
|
|
// and link the new node to the original node's parent as a sibling
|
|
|
|
// and link the new node to the original node's parent as a sibling
|
|
|
|
|
|
|
|
const newNode = new Node(bestSplit.rightEntries, node.isLeaf, node.parent);
|
|
|
|
|
|
|
|
if (!node.isLeaf)
|
|
|
|
|
|
|
|
bestSplit.rightEntries.forEach(entry => entry.node.parent = newNode);
|
|
|
|
|
|
|
|
|
|
|
|
const rightEntry = new RoutingEntry(newNode, this);
|
|
|
|
const rightEntry = new RoutingEntry(newNode, this);
|
|
|
|
|
|
|
|
node.entries = bestSplit.leftEntries;
|
|
|
|
node.parent.entries.push(rightEntry);
|
|
|
|
node.parent.entries.push(rightEntry);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const nodeEntry = node.parent.entries.find(entry => entry.node === node);
|
|
|
|
|
|
|
|
if (nodeEntry) {
|
|
|
|
|
|
|
|
nodeEntry.updateCentroid();
|
|
|
|
|
|
|
|
nodeEntry.updateRadius();
|
|
|
|
|
|
|
|
}
|
|
|
|
if (node.parent.entries.length > this.capacity)
|
|
|
|
if (node.parent.entries.length > this.capacity)
|
|
|
|
// If the parent node now has more than capacity entries, split it too
|
|
|
|
// If the parent node now has more than capacity entries, split it too
|
|
|
|
this.splitNode(node.parent);
|
|
|
|
this.splitNode(node.parent);
|
|
|
|
|
|
|
|
|
|
|
|
node.entries = bestSplit.leftEntries;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
// If the node is a root node, split it into two new nodes
|
|
|
|
// If the node is a root node, split it into two new nodes
|
|
|
|
this.splitRoot(node);
|
|
|
|
const leftNode = new Node(bestSplit.leftEntries, node.isLeaf);
|
|
|
|
|
|
|
|
const rightNode = new Node(bestSplit.rightEntries, node.isLeaf);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
leftNode.parent = node;
|
|
|
|
|
|
|
|
rightNode.parent = node;
|
|
|
|
|
|
|
|
if (!node.isLeaf) {
|
|
|
|
|
|
|
|
bestSplit.leftEntries.forEach(entry => entry.node.parent = leftNode);
|
|
|
|
|
|
|
|
bestSplit.rightEntries.forEach(entry => entry.node.parent = rightNode);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const rightEntry = new RoutingEntry(rightNode, this);
|
|
|
|
|
|
|
|
const leftEntry = new RoutingEntry(leftNode, this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
node.entries = [leftEntry, rightEntry];
|
|
|
|
|
|
|
|
node.isLeaf = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -216,11 +208,11 @@ class MTree {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Returns all points in the tree that are within the given radius of the given query point.
|
|
|
|
* Returns all points in the tree that are within the given radius of the given query point.
|
|
|
|
* @param {number[]} queryPoint - The point to search around.
|
|
|
|
* @param {number[]} queryPoint - The point to search around.
|
|
|
|
* @param {number} radius - The radius to search within.
|
|
|
|
* @param {number} radius - The radius to search within.
|
|
|
|
* @returns {number[][]} An array of points that are within the radius of the query point.
|
|
|
|
* @returns {number[][]} An array of points that are within the radius of the query point.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
rangeQuery(queryPoint, radius) {
|
|
|
|
rangeQuery(queryPoint, radius) {
|
|
|
|
//console.log(`rangeQuery: queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`);
|
|
|
|
//console.log(`rangeQuery: queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`);
|
|
|
|
const result = [];
|
|
|
|
const result = [];
|
|
|
|
@ -235,7 +227,6 @@ class MTree {
|
|
|
|
* @param {number[]} queryPoint - The point to search around
|
|
|
|
* @param {number[]} queryPoint - The point to search around
|
|
|
|
* @param {number} radius - The radius to search within
|
|
|
|
* @param {number} radius - The radius to search within
|
|
|
|
* @param {number[][]} result - An array to store the result in
|
|
|
|
* @param {number[][]} result - An array to store the result in
|
|
|
|
* @returns {undefined}
|
|
|
|
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
rangeQueryRecursive(node, queryPoint, radius, result) {
|
|
|
|
rangeQueryRecursive(node, queryPoint, radius, result) {
|
|
|
|
//console.log(`rangeQueryRecursive: node=${node.id}, queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`);
|
|
|
|
//console.log(`rangeQueryRecursive: node=${node.id}, queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`);
|
|
|
|
@ -250,15 +241,15 @@ class MTree {
|
|
|
|
for (const entry of node.entries) {
|
|
|
|
for (const entry of node.entries) {
|
|
|
|
//console.log(`rangeQueryRecursive: checking routing entry ${entry}`);
|
|
|
|
//console.log(`rangeQueryRecursive: checking routing entry ${entry}`);
|
|
|
|
const distance = this.distanceFunction(queryPoint, entry.point);
|
|
|
|
const distance = this.distanceFunction(queryPoint, entry.point);
|
|
|
|
if (distance > radius + entry.radius) {
|
|
|
|
if (distance <= radius + entry.radius)
|
|
|
|
//console.log(`rangeQueryRecursive: regions do not overlap, stopping the recursion on this branch`);
|
|
|
|
this.rangeQueryRecursive(entry.node, queryPoint, radius, result);
|
|
|
|
continue;
|
|
|
|
//else
|
|
|
|
}
|
|
|
|
// console.log(`rangeQueryRecursive: skipping node ${entry.node.id} due to distance ${distance} > ${radius + entry.radius}`);
|
|
|
|
this.rangeQueryRecursive(entry.node, queryPoint, radius, result);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Finds the k nearest neighbors to a given query point.
|
|
|
|
* Finds the k nearest neighbors to a given query point.
|
|
|
|
* @param {number[]} queryPoint - The point to find the nearest neighbors to
|
|
|
|
* @param {number[]} queryPoint - The point to find the nearest neighbors to
|
|
|
|
@ -284,9 +275,8 @@ class MTree {
|
|
|
|
for (const entry of node.entries) {
|
|
|
|
for (const entry of node.entries) {
|
|
|
|
const distance = this.distanceFunction(queryPoint, entry.point);
|
|
|
|
const distance = this.distanceFunction(queryPoint, entry.point);
|
|
|
|
if (distance < result[k - 1].distance) {
|
|
|
|
if (distance < result[k - 1].distance) {
|
|
|
|
result.push({ point: entry.point, distance });
|
|
|
|
result[k - 1] = { point: entry.point, distance: distance };
|
|
|
|
result.sort((a, b) => a.distance - b.distance);
|
|
|
|
result.sort((a, b) => a.distance - b.distance);
|
|
|
|
result.pop();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
@ -302,3 +292,4 @@ class MTree {
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = MTree;
|
|
|
|
module.exports = MTree;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|