diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..16542d5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+package-lock.json
+npm-debug.log
+.vscode/
\ No newline at end of file
diff --git a/data/generator.js b/data/generator.js
new file mode 100644
index 0000000..b58f4fd
--- /dev/null
+++ b/data/generator.js
@@ -0,0 +1,30 @@
+class Generator {
+ /**
+ * Create a new generator
+ * @param {number} dimensions - The number of dimensions for the random vectors
+ */
+ constructor(dimensions) {
+ this.dimensions = dimensions;
+ }
+
+ /**
+ * Generates a random vector with the specified number of dimensions.
+ * Each element of the vector is a random number between 0 and 1.
+ * @returns {number[]} A random vector of the given dimensions
+ */
+ generate() {
+ return Array.from({ length: this.dimensions }, () => Math.random());
+ }
+
+ /**
+ * Generates an array of a specified length, each element of which is a
+ * random vector of the given dimension.
+ * @param {number} count - The number of vectors to generate
+ * @returns {number[][]} an array of random vectors
+ */
+ generateMany(count) {
+ return Array.from({ length: count }, () => this.generate());
+ }
+}
+
+module.exports = Generator;
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..ba6c077
--- /dev/null
+++ b/index.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+ MTree API Interface
+
+
+
+
+
MTree API Interface
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..4951043
--- /dev/null
+++ b/index.js
@@ -0,0 +1,74 @@
+const express = require('express');
+const bodyParser = require('body-parser');
+const MTree = require('./m-tree/mtree');
+const dimensions = 2;
+
+const Generator = require('./data/generator');
+
+const app = express();
+app.use(bodyParser.json());
+
+
+function euclideanDistance(a, b) {
+ return Math.sqrt(a.reduce((acc, val, i) => acc + (val - b[i]) ** 2, 0));
+}
+const mtree = new MTree(dimensions, 10, euclideanDistance);
+
+const generator = new Generator(dimensions);
+const points = generator.generateMany(1000);
+let i = 0;
+points.forEach(point => { mtree.insert(point); i++; console.log(i); });
+
+app.get('/', (req, res) => {
+ res.sendFile(__dirname + '/index.html');
+});
+
+app.get('/tree', (req, res) => {
+ res.send(JSON.parse(JSON.stringify(mtree, (key, value) => {
+ if (key === 'parent') return value && value.id;
+ return value;
+ })));
+});
+
+app.post('/insert', (req, res) => {
+ const point = req.body.point;
+ if (!point || !Array.isArray(point)) {
+ return res.status(400).send('Invalid point');
+ }
+ mtree.insert(point);
+ res.send('Point inserted');
+});
+
+app.get('/rangeQuery', (req, res) => {
+ const { queryPoint, radius } = req.query;
+ if (!queryPoint || !radius) {
+ return res.status(400).send('Invalid query parameters');
+ }
+ const result = mtree.rangeQuery(JSON.parse(queryPoint), parseFloat(radius));
+ res.send(result);
+});
+
+app.get('/kNNQuery', (req, res) => {
+ const { queryPoint, k } = req.query;
+ if (!queryPoint || !k) {
+ return res.status(400).send('Invalid query parameters');
+ }
+ const result = mtree.kNNQuery(JSON.parse(queryPoint), parseInt(k, 10));
+ res.send(result);
+});
+
+app.listen(3000, () => {
+ console.log('MTree API is running on port 3000');
+ //console.log(mtree);
+});
+
+app.post('/recreate', (req, res) => {
+ const { dimensions } = req.body;
+ if (!dimensions || typeof dimensions !== 'number') {
+ return res.status(400).send('Invalid dimensions');
+ }
+ mtree = new MTree(dimensions, 10);
+ points = generator.generateMany(100);
+ points.forEach(point => mtree.insert(point));
+ res.send('MTree recreated');
+});
diff --git a/m-tree/mtree.js b/m-tree/mtree.js
new file mode 100644
index 0000000..0805f47
--- /dev/null
+++ b/m-tree/mtree.js
@@ -0,0 +1,254 @@
+const { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry } = require('./nodes');
+
+class MTree {
+ /**
+ * Constructs a new MTree instance.
+ * @param {number} dimensions - The number of dimensions for data points.
+ * @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.
+ */
+ constructor(dimensions, capacity, distanceFunction = (a, b) => Math.sqrt(a.map((x, i) => (x - b[i]) ** 2).reduce((sum, x) => sum + x))) {
+ this.dimensions = dimensions;
+ this.capacity = capacity;
+ this.distanceFunction = distanceFunction;
+ this.root = new LeafNode([], null);
+ }
+
+ /**
+ * Inserts a new point into the MTree.
+ * @param {number[]} point - The point to insert
+ */
+ insert(point) {
+ const node = this.findLeafNode(this.root, point);
+ if (!node) {
+ throw new Error('No leaf node found for insertion');
+ }
+
+ const groundEntry = new GroundEntry(point);
+ node.entries.push(groundEntry);
+ console.log(node.entries.length, this.capacity);
+ if (node.entries.length > this.capacity) {
+ this.splitNode(node);
+ }
+ }
+
+ /**
+ * Recursively traverses the MTree from the given node to find the leaf node
+ * that should hold the given point.
+ * @param {Node} node - The node to start searching from
+ * @param {number[]} point - The point to find the leaf node for
+ * @returns {LeafNode} The leaf node that should hold the given point
+ */
+ findLeafNode(node, point) {
+ if (!node) {
+ throw new Error('Node is undefined');
+ }
+ if (node.isLeaf) {
+ return node;
+ }
+ const closestEntry = this.findClosestEntry(node.entries, point);
+ if (!closestEntry) {
+ return node;
+ }
+ return this.findLeafNode(closestEntry.node, point);
+ }
+
+ /**
+ * Finds the entry in the given array of entries that is closest to the given point.
+ * @param {NodeEntry[]} entries - The array of entries to search through
+ * @param {number[]} point - The point to find the closest entry to
+ * @returns {NodeEntry} The closest entry to the given point
+ */
+ findClosestEntry(entries, point) {
+ let closestEntry = null;
+ let minDistance = Infinity;
+
+ for (const entry of entries) {
+ const distance = this.distanceFunction(point, entry.point);
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestEntry = entry;
+ }
+ }
+ 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 mid = Math.ceil(entries.length / 2);
+ const leftEntries = entries.slice(0, mid);
+ const rightEntries = entries.slice(mid);
+ const leftNode = new LeafNode(leftEntries);
+ const rightNode = new LeafNode(rightEntries);
+ leftNode.parent = node; // Set parent of leftNode
+ rightNode.parent = node; // Set parent of rightNode
+
+ const leftCentroid = this.calculateCentroid(leftEntries);
+ const rightCentroid = this.calculateCentroid(rightEntries);
+ 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.isLeaf = false;
+ }
+
+ /**
+ * Splits the given node into two nodes when the number of entries exceeds capacity.
+ * The given node will be left with half of the entries, and a new node is created with the other half.
+ * 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.
+ */
+ splitNode(node) {
+ if (!node) {
+ throw new Error('Node is undefined');
+ }
+ const entries = node.entries;
+ const mid = Math.ceil(entries.length / 2);
+ const leftEntries = entries.slice(0, mid);
+ const rightEntries = entries.slice(mid);
+ const newNode = new Node(rightEntries, node.isLeaf, node.parent);
+
+ if (node.parent) {
+ const rightCentroid = this.calculateCentroid(rightEntries);
+ const rightRadius = this.calculateRadius(rightEntries, rightCentroid);
+ const rightEntry = new RoutingEntry(rightCentroid, newNode, rightRadius);
+ node.parent.entries.push(rightEntry);
+
+ if (node.parent.entries.length > this.capacity)
+ this.splitNode(node.parent);
+
+ node.entries = leftEntries;
+ } else {
+ this.splitRoot(node);
+ }
+ }
+
+ /**
+ * Calculates the centroid of a given set of entries.
+ * The centroid is the average position of all the points in the entries, calculated dimension-wise.
+ *
+ * @param {Object[]} entries - An array of entries, each with a 'point' property that is an array of numbers.
+ * @returns {number[]} The centroid of the entries as an array of numbers representing the average position.
+ */
+ calculateCentroid(entries) {
+ const centroid = new Array(this.dimensions).fill(0);
+ 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);
+ }
+
+ /**
+ * 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)));
+ }
+
+ /**
+ * 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} radius - The radius to search within.
+ * @returns {number[][]} An array of points that are within the radius of the query point.
+ */
+ rangeQuery(queryPoint, radius) {
+ console.log(`rangeQuery: queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`);
+ const result = [];
+ this.rangeQueryRecursive(this.root, queryPoint, radius, result);
+ console.log(`rangeQuery: result=${JSON.stringify(result)}`);
+ return result;
+ }
+
+ /**
+ * Recursively traverses the MTree to find all points within the given radius of the given query point.
+ * @param {Node} node - The node to start searching from
+ * @param {number[]} queryPoint - The point to search around
+ * @param {number} radius - The radius to search within
+ * @param {number[][]} result - An array to store the result in
+ * @returns {undefined}
+ */
+ rangeQueryRecursive(node, queryPoint, radius, result) {
+ console.log(`rangeQueryRecursive: node=${node.id}, queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`);
+ if (node.isLeaf) {
+ for (const entry of node.entries) {
+ console.log(`rangeQueryRecursive: checking ground entry ${entry}`);
+ if (entry instanceof GroundEntry && this.distanceFunction(queryPoint, entry.point) <= radius) {
+ result.push(entry.point);
+ }
+ }
+ } else {
+ for (const entry of node.entries) {
+ console.log(`rangeQueryRecursive: checking routing entry ${entry}`);
+ const distance = this.distanceFunction(queryPoint, entry.point);
+ if (distance > radius + entry.radius) {
+ console.log(`rangeQueryRecursive: regions do not overlap, stopping the recursion on this branch`);
+ continue;
+ }
+ this.rangeQueryRecursive(entry.node, queryPoint, radius, result);
+ }
+ }
+ }
+
+ /**
+ * Finds the k nearest neighbors to a given query point.
+ * @param {number[]} queryPoint - The point to find the nearest neighbors to
+ * @param {number} k - The number of nearest neighbors to find
+ * @returns {Object[]} An array of objects with 'point' and 'distance' properties, sorted by distance.
+ */
+ kNNQuery(queryPoint, k) {
+ const result = Array(k).fill({ distance: Infinity });
+ this.kNNQueryRecursive(this.root, queryPoint, k, result);
+ return result;
+ }
+
+ /**
+ * Recursively traverses the MTree to find the k nearest neighbors to a given query point.
+ * @param {Node} node - The node to start searching from
+ * @param {number[]} queryPoint - The point to find the nearest neighbors to
+ * @param {number} k - The number of nearest neighbors to find
+ * @param {Object[]} result - The result array to store the k nearest neighbors in, sorted by distance.
+ * @returns {undefined}
+ */
+ kNNQueryRecursive(node, queryPoint, k, result) {
+ if (node.isLeaf) {
+ for (const entry of node.entries) {
+ const distance = this.distanceFunction(queryPoint, entry.point);
+ if (distance < result[k - 1].distance) {
+ result.push({ point: entry.point, distance });
+ result.sort((a, b) => a.distance - b.distance);
+ result.pop();
+ }
+ }
+ } else {
+ for (const entry of node.entries) {
+ const distance = this.distanceFunction(queryPoint, entry.point);
+ if (distance <= result[k - 1].distance + entry.radius) {
+ this.kNNQueryRecursive(entry.node, queryPoint, k, result);
+ }
+ }
+ }
+ }
+}
+
+module.exports = MTree;
+
diff --git a/m-tree/nodes.js b/m-tree/nodes.js
new file mode 100644
index 0000000..5347fda
--- /dev/null
+++ b/m-tree/nodes.js
@@ -0,0 +1,68 @@
+class RoutingEntry {
+ /**
+ * Create a new routing entry
+ * @param {number[]} point - The point of the entry
+ * @param {Node} node - The node associated with the entry
+ * @param {number} radius - The radius of the entry
+ */
+ constructor(point, node, radius = 0) {
+ this.point = point;
+ this.node = node;
+ this.radius = radius;
+ }
+}
+
+class GroundEntry {
+ /**
+ * Create a new group entry
+ * @param {number[]} point - The point of the entry
+ * @param {number} data - The data associated with the entry
+ */
+ constructor(point, data = null) {
+ this.point = point;
+ this.data = data;
+ }
+}
+
+class Node {
+ static idCounter = 0; // Initialize a static counter
+
+ /**
+ * Create a new node
+ * @param {GroundEntry[]|RoutingEntry[]} entries - The entries in the node
+ * @param {boolean} isLeaf - Whether the node is a leaf node
+ * @param {Node} parent - The parent node
+ */
+ constructor(entries, isLeaf, parent) {
+ this.id = Node.idCounter++; // Assign a unique ID to the node
+ this.entries = entries;
+ this.isLeaf = isLeaf;
+ this.parent = parent;
+ //this.next = null;
+ //this.prev = null;
+ }
+}
+
+class LeafNode extends Node {
+ /**
+ * Create a new leaf node
+ * @param {GroundEntry[]} entries - The entries in the node
+ * @param {Node} parent - The parent node
+ */
+ constructor(entries, parent) {
+ super(entries, true, parent); // Call the Node constructor with isLeaf set to true
+ }
+}
+
+class InternalNode extends Node {
+ /**
+ * Create a new internal node
+ * @param {RoutingEntry[]} entries - The entries in the node
+ * @param {Node} parent - The parent node
+ */
+ constructor(entries, parent) {
+ super(entries, false, parent); // Call the Node constructor with isLeaf set to false
+ }
+}
+
+module.exports = { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry };
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b650136
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "ni-vmm",
+ "version": "1.0.0",
+ "description": "Basic backend for M-Trees",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "node index.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git@gitlab.fit.cvut.cz:spacefr1/ni-vmm.git"
+ },
+ "author": "František Špaček",
+ "license": "ISC",
+ "dependencies": {
+ "express": "^4.21.1"
+ }
+}