|
|
|
@ -3,12 +3,12 @@ const { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry } = require('./n
|
|
|
|
class MTree {
|
|
|
|
class MTree {
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Constructs a new MTree instance.
|
|
|
|
* Constructs a new MTree instance.
|
|
|
|
* @param {number} dimensions - The number of dimensions for data points.
|
|
|
|
* @param {number} dimension - The number of dimension for data points.
|
|
|
|
* @param {number} capacity - The maximum number of entries a node can hold before splitting.
|
|
|
|
* @param {number} capacity - The maximum number of entries a node can hold before splitting.
|
|
|
|
* @param {function} [distanceFunction] - A function to calculate the distance between two points. Defaults to Euclidean distance.
|
|
|
|
* @param {function} [distanceFunction] - A function to calculate the distance between two points. Defaults to Euclidean distance.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
constructor(dimensions, capacity, distanceFunction = (a, b) => Math.sqrt(a.map((x, i) => (x - b[i]) ** 2).reduce((sum, x) => sum + x))) {
|
|
|
|
constructor(dimension, capacity, distanceFunction = (a, b) => Math.sqrt(a.map((x, i) => (x - b[i]) ** 2).reduce((sum, x) => sum + x))) {
|
|
|
|
this.dimensions = dimensions;
|
|
|
|
this.dimension = dimension;
|
|
|
|
this.capacity = capacity;
|
|
|
|
this.capacity = capacity;
|
|
|
|
this.distanceFunction = distanceFunction;
|
|
|
|
this.distanceFunction = distanceFunction;
|
|
|
|
this.root = new LeafNode([], null);
|
|
|
|
this.root = new LeafNode([], null);
|
|
|
|
@ -30,6 +30,16 @@ class MTree {
|
|
|
|
if (node.entries.length > this.capacity) {
|
|
|
|
if (node.entries.length > this.capacity) {
|
|
|
|
this.splitNode(node);
|
|
|
|
this.splitNode(node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update radius for each routing entry from leaf to root
|
|
|
|
|
|
|
|
let currentNode = node;
|
|
|
|
|
|
|
|
while (currentNode.parent) {
|
|
|
|
|
|
|
|
const routingEntry = currentNode.parent.entries.find(entry => entry.node === currentNode);
|
|
|
|
|
|
|
|
if (routingEntry) {
|
|
|
|
|
|
|
|
routingEntry.updateRadius();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
currentNode = currentNode.parent;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -85,84 +95,124 @@ class MTree {
|
|
|
|
if (!node) {
|
|
|
|
if (!node) {
|
|
|
|
throw new Error('Node is undefined');
|
|
|
|
throw new Error('Node is undefined');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const entries = node.entries;
|
|
|
|
const entries = node.entries;
|
|
|
|
const mid = Math.ceil(entries.length / 2);
|
|
|
|
const halfSize = Math.floor(entries.length / 2);
|
|
|
|
const leftEntries = entries.slice(0, mid);
|
|
|
|
|
|
|
|
const rightEntries = entries.slice(mid);
|
|
|
|
let minRadius = Infinity;
|
|
|
|
const leftNode = new LeafNode(leftEntries);
|
|
|
|
let bestSplit = null;
|
|
|
|
const rightNode = new LeafNode(rightEntries);
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
leftNode.parent = node; // Set parent of leftNode
|
|
|
|
rightNode.parent = node; // Set parent of rightNode
|
|
|
|
rightNode.parent = node; // Set parent of rightNode
|
|
|
|
|
|
|
|
|
|
|
|
const leftCentroid = this.calculateCentroid(leftEntries);
|
|
|
|
const rightEntry = new RoutingEntry(rightNode, this);
|
|
|
|
const rightCentroid = this.calculateCentroid(rightEntries);
|
|
|
|
const leftEntry = new RoutingEntry(leftNode, this);
|
|
|
|
const leftRadius = this.calculateRadius(leftEntries, leftCentroid);
|
|
|
|
|
|
|
|
const rightRadius = this.calculateRadius(rightEntries, rightCentroid);
|
|
|
|
|
|
|
|
const leftEntry = new RoutingEntry(leftCentroid, leftNode, leftRadius);
|
|
|
|
|
|
|
|
const rightEntry = new RoutingEntry(rightCentroid, rightNode, rightRadius);
|
|
|
|
|
|
|
|
node.entries = [leftEntry, rightEntry];
|
|
|
|
node.entries = [leftEntry, rightEntry];
|
|
|
|
node.isLeaf = false;
|
|
|
|
node.isLeaf = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Splits the given node into two nodes when the number of entries exceeds capacity.
|
|
|
|
* Calculates the radius of the given entries.
|
|
|
|
* The given node will be left with half of the entries, and a new node is created with the other half.
|
|
|
|
* The radius is the maximum distance from the centroid of the entries to any point in the entries.
|
|
|
|
* The new node is linked to the original node's parent as a sibling by calculating and using the centroid
|
|
|
|
|
|
|
|
* as the connecting entry point.
|
|
|
|
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param {Node} node - The node to be split.
|
|
|
|
* @param {Object[]} entries - The entries to calculate the radius from.
|
|
|
|
|
|
|
|
* @returns {number} The radius.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
|
|
|
|
calculateRadius(entries) {
|
|
|
|
|
|
|
|
// Calculate the centroid of the entries
|
|
|
|
|
|
|
|
const sum = entries.reduce((acc, entry) => {
|
|
|
|
|
|
|
|
return acc.map((val, i) => val + entry.point[i]);
|
|
|
|
|
|
|
|
}, new Array(entries[0].point.length).fill(0));
|
|
|
|
|
|
|
|
const centroidPoint = sum.map(val => val / entries.length);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate the maximum distance from the centroid to any point in the entries
|
|
|
|
|
|
|
|
const maxDistance = Math.max(...entries.map(entry => this.distanceFunction(entry.point, centroidPoint)));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return maxDistance;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
splitNode(node) {
|
|
|
|
splitNode(node) {
|
|
|
|
if (!node) {
|
|
|
|
|
|
|
|
throw new Error('Node is undefined');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const entries = node.entries;
|
|
|
|
const entries = node.entries;
|
|
|
|
const mid = Math.ceil(entries.length / 2);
|
|
|
|
const halfSize = Math.floor(entries.length / 2);
|
|
|
|
const leftEntries = entries.slice(0, mid);
|
|
|
|
|
|
|
|
const rightEntries = entries.slice(mid);
|
|
|
|
let minRadius = Infinity;
|
|
|
|
const newNode = new Node(rightEntries, node.isLeaf, node.parent);
|
|
|
|
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 newNode = new Node(bestSplit.rightEntries, node.isLeaf, node.parent);
|
|
|
|
|
|
|
|
|
|
|
|
if (node.parent) {
|
|
|
|
if (node.parent) {
|
|
|
|
const rightCentroid = this.calculateCentroid(rightEntries);
|
|
|
|
// If the node is not a root node, split it into two nodes
|
|
|
|
const rightRadius = this.calculateRadius(rightEntries, rightCentroid);
|
|
|
|
// and link the new node to the original node's parent as a sibling
|
|
|
|
const rightEntry = new RoutingEntry(rightCentroid, newNode, rightRadius);
|
|
|
|
const rightEntry = new RoutingEntry(newNode, this);
|
|
|
|
node.parent.entries.push(rightEntry);
|
|
|
|
node.parent.entries.push(rightEntry);
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
this.splitNode(node.parent);
|
|
|
|
this.splitNode(node.parent);
|
|
|
|
|
|
|
|
|
|
|
|
node.entries = leftEntries;
|
|
|
|
node.entries = bestSplit.leftEntries;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// If the node is a root node, split it into two new nodes
|
|
|
|
this.splitRoot(node);
|
|
|
|
this.splitRoot(node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
// Helper function to generate all combinations of a given size
|
|
|
|
* Calculates the centroid of a given set of entries.
|
|
|
|
getCombinations(arr, size) {
|
|
|
|
* The centroid is the average position of all the points in the entries, calculated dimension-wise.
|
|
|
|
if (size === 0) return [[]];
|
|
|
|
*
|
|
|
|
const combinations = [];
|
|
|
|
* @param {Object[]} entries - An array of entries, each with a 'point' property that is an array of numbers.
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
|
|
* @returns {number[]} The centroid of the entries as an array of numbers representing the average position.
|
|
|
|
const current = arr[i];
|
|
|
|
*/
|
|
|
|
const rest = arr.slice(i + 1);
|
|
|
|
calculateCentroid(entries) {
|
|
|
|
for (const combination of this.getCombinations(rest, size - 1)) {
|
|
|
|
const centroid = new Array(this.dimensions).fill(0);
|
|
|
|
combinations.push([current, ...combination]);
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
|
|
|
for (let i = 0; i < this.dimensions; i++) {
|
|
|
|
|
|
|
|
centroid[i] += entry.point[i];
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return centroid.map(x => x / entries.length);
|
|
|
|
return combinations;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Calculates the maximum distance from the given centroid to any of the given entries.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* @param {Object[]} entries - An array of entries, each with a 'point' property that is an array of numbers.
|
|
|
|
|
|
|
|
* @param {number[]} centroid - The centroid to calculate the distance from.
|
|
|
|
|
|
|
|
* @returns {number} The maximum distance from the centroid to any of the entries.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
calculateRadius(entries, centroid) {
|
|
|
|
|
|
|
|
return Math.max(...entries.map(entry => this.distanceFunction(entry.point, centroid)));
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -172,10 +222,10 @@ class MTree {
|
|
|
|
* @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 = [];
|
|
|
|
this.rangeQueryRecursive(this.root, queryPoint, radius, result);
|
|
|
|
this.rangeQueryRecursive(this.root, queryPoint, radius, result);
|
|
|
|
console.log(`rangeQuery: result=${JSON.stringify(result)}`);
|
|
|
|
//console.log(`rangeQuery: result=${JSON.stringify(result)}`);
|
|
|
|
return result;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -188,20 +238,20 @@ class MTree {
|
|
|
|
* @returns {undefined}
|
|
|
|
* @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}`);
|
|
|
|
if (node.isLeaf) {
|
|
|
|
if (node.isLeaf) {
|
|
|
|
for (const entry of node.entries) {
|
|
|
|
for (const entry of node.entries) {
|
|
|
|
console.log(`rangeQueryRecursive: checking ground entry ${entry}`);
|
|
|
|
//console.log(`rangeQueryRecursive: checking ground entry ${entry}`);
|
|
|
|
if (entry instanceof GroundEntry && this.distanceFunction(queryPoint, entry.point) <= radius) {
|
|
|
|
if (entry instanceof GroundEntry && this.distanceFunction(queryPoint, entry.point) <= radius) {
|
|
|
|
result.push(entry.point);
|
|
|
|
result.push(entry.point);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
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`);
|
|
|
|
//console.log(`rangeQueryRecursive: regions do not overlap, stopping the recursion on this branch`);
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.rangeQueryRecursive(entry.node, queryPoint, radius, result);
|
|
|
|
this.rangeQueryRecursive(entry.node, queryPoint, radius, result);
|
|
|
|
|