diff --git a/index.html b/index.html
deleted file mode 100644
index f21c259..0000000
--- a/index.html
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
-
-
-
- MTree API Interface
-
-
-
-
- MTree API Interface
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/index.js b/index.js
index d6bd942..21f9c07 100644
--- a/index.js
+++ b/index.js
@@ -1,7 +1,6 @@
const express = require('express');
const bodyParser = require('body-parser');
const MTree = require('./m-tree/mtree');
-const dimensions = 2;
// Generator used to generate random points for the MTree
const Generator = require('./data/generator');
@@ -16,34 +15,37 @@ function euclideanDistance(a, b) {
}
// Create an MTree with the given dimensions and capacity, using the Euclidean distance
-const mtree = new MTree(dimensions, 10, euclideanDistance);
+let mtree = null;
-// Generate 1000 random points
-const generator = new Generator(dimensions);
-const points = generator.generateMany(10000);
+// Serve static files from the 'public' directory
+app.use(express.static(__dirname + '/public'));
-// Insert all points into the MTree
-points.forEach(point => mtree.insert(point));
+// Respond to favicon requests with no content
+app.get('/favicon.ico', (req, res) => { res.status(204).end(); });
// Serve the index.html file
-app.get('/', (req, res) => {
- res.sendFile(__dirname + '/index.html');
-});
+app.get('/', (req, res) => { res.sendFile(__dirname + '/public/index.html'); });
// Return the MTree
app.get('/tree', (req, res) => {
res.send(JSON.parse(JSON.stringify(mtree, (key, value) => {
- if (key === 'parent' || key === 'mtree') return value && value.id;
+ if (key === 'parent' || key === 'mtree')
+ return value && value.id;
return value;
})));
});
+// Endpoint to check if the MTree has been initialized
+app.get('/isInitialized', (req, res) => { res.send(!!mtree); });
+
// Insert a point into the MTree
app.post('/insert', (req, res) => {
- const point = req.body.point;
- if (!point || !Array.isArray(point)) {
- return res.status(400).send('Invalid point');
- }
+ let point = req.body.point;
+ if (!point)
+ return res.status(400).send('Missing point');
+ if (point.length !== mtree.dimension)
+ return res.status(400).send('Point has the wrong dimension');
+
mtree.insert(point);
res.send('Point inserted');
});
@@ -51,26 +53,32 @@ app.post('/insert', (req, res) => {
// Perform a range query on the MTree
app.get('/rangeQuery', (req, res) => {
const { queryPoint, radius } = req.query;
- if (!queryPoint || !radius || JSON.parse(queryPoint).length !== dimensions) {
- return res.status(400).send('Invalid query parameters');
+ if (!queryPoint)
+ return res.status(400).send('Missing query point');
+ if (!radius || isNaN(radius))
+ return res.status(400).send('Missing radius');
+ if (JSON.parse(queryPoint).length !== mtree.dimension)
+ return res.status(400).send('Query point has the wrong dimension');
+ let parsedQueryPoint;
+ try {
+ parsedQueryPoint = JSON.parse(queryPoint);
+ } catch (e) {
+ return res.status(400).send('Invalid query point');
}
+ const parsedRadius = parseFloat(radius);
+
+ if (parsedQueryPoint.length !== mtree.dimension)
+ return res.status(400).send('Query point has the wrong dimension');
+
console.time('mtreeRangeQuery');
- const result = mtree.rangeQuery(JSON.parse(queryPoint), parseFloat(radius));
+ const result = mtree.rangeQuery(parsedQueryPoint, parsedRadius);
console.timeEnd('mtreeRangeQuery');
- console.log(result);
+ //console.log(result.length);
console.time('sequentialSearch');
- const sequentialResult = points.filter(point => euclideanDistance(point, JSON.parse(queryPoint)) <= parseFloat(radius));
+ const sequentialResult = points.filter(point => euclideanDistance(point, parsedQueryPoint) <= parsedRadius);
console.timeEnd('sequentialSearch');
- //console.log(sequentialResult.map(point => "point:" + point +" distance: " + euclideanDistance(point, JSON.parse(queryPoint))));
- console.log(sequentialResult);
-
- /*const furthestPoint = points.reduce((furthest, point) => {
- const distance = euclideanDistance(point, mtree.root.entries[0].point);
- return distance > euclideanDistance(furthest, mtree.root.entries[0].point) ? point : furthest;
- }, points[0]);
- console.log(`distance between first routing entry centroid and furthest point: ${euclideanDistance(furthestPoint, mtree.root.entries[0].point)}`);
- console.log(`radius of first routing entry: ${furthestPoint, mtree.root.entries[0].radius}`);*/
+ //console.log(sequentialResult.length);
res.send(result);
});
@@ -78,39 +86,61 @@ app.get('/rangeQuery', (req, res) => {
// Perform a k-NN query on the MTree
app.get('/kNNQuery', (req, res) => {
const { queryPoint, k } = req.query;
- if (!queryPoint || !k) {
- return res.status(400).send('Invalid query parameters');
+ if (!queryPoint)
+ return res.status(400).send('Missing query point');
+ if (!k || isNaN(k))
+ return res.status(400).send('Missing k');
+ if (JSON.parse(queryPoint).length !== mtree.dimension)
+ return res.status(400).send('Query point has the wrong dimension');
+ let parsedQueryPoint;
+ try {
+ parsedQueryPoint = JSON.parse(queryPoint);
+ } catch (e) {
+ return res.status(400).send('Invalid query point');
}
- const parsedQueryPoint = JSON.parse(queryPoint);
const kInt = parseInt(k, 10);
- console.time('mtreeSearch');
+ console.time('mtreeKNNQuery');
const result = mtree.kNNQuery(parsedQueryPoint, kInt);
- console.timeEnd('mtreeSearch');
+ console.timeEnd('mtreeKNNQuery');
+ //console.log(result);
console.time('sequentialSearch');
const sequentialResult = points.sort((a, b) => euclideanDistance(a, parsedQueryPoint) - euclideanDistance(b, parsedQueryPoint)).slice(0, kInt);
console.timeEnd('sequentialSearch');
-
- console.log(sequentialResult);
+ //console.log(sequentialResult);
res.send(result);
});
-// Start the server
-app.listen(3000, () => {
- console.log('MTree API is running on port 3000');
-});
-
// Recreate the MTree with the given dimensions
app.post('/recreate', (req, res) => {
- const { dimensions } = req.body;
- if (!dimensions || typeof dimensions !== 'number') {
+ let { dimensions, pointCount } = req.body;
+ dimensions = parseInt(dimensions);
+ pointCount = parseInt(pointCount);
+ console.log(dimensions, pointCount);
+ if (isNaN(dimensions) || dimensions <= 0)
return res.status(400).send('Invalid dimensions');
- }
+ if (isNaN(pointCount) || pointCount <= 0)
+ return res.status(400).send('Invalid point count');
+
+ const generator = new Generator(dimensions);
mtree = new MTree(dimensions, 10);
- points = generator.generateMany(100);
- points.forEach(point => mtree.insert(point));
+ points = generator.generateMany(pointCount);
+
+ // points.forEach(point => mtree.insert(point));
+ points.forEach((point, i) => {
+ if (i % 100 === 0) {
+ console.time(`insertion of next 100 points`);
+ console.log(`inserted ${i} points`);
+ }
+ mtree.insert(point);
+ if (i % 100 === 99)
+ console.timeEnd(`insertion of next 100 points`);
+ })
+
res.send('MTree recreated');
});
+// Start the server
+app.listen(3000, () => { console.log('MTree API is running on port 3000'); });
\ No newline at end of file
diff --git a/m-tree/mtree.js b/m-tree/mtree.js
index ae7aa66..c57f622 100644
--- a/m-tree/mtree.js
+++ b/m-tree/mtree.js
@@ -31,19 +31,9 @@ class MTree {
// Add the ground entry to the leaf node's entries
leafNode.entries.push(groundEntry);
- // 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();
- }
- }
-
// If the leaf node now has more than the capacity amount of entries, split it
- if (leafNode.entries.length > this.capacity) {
+ if (leafNode.entries.length > this.capacity)
this.splitNode(leafNode);
- }
//console.log(`found leaf node: ${leafNode.id}`);
// Update the routing entries of ancestors of the leaf node
@@ -67,18 +57,16 @@ class MTree {
* @returns {LeafNode} The leaf node that should hold the given point
*/
findLeafNode(node, point) {
- if (!node) {
+ if (!node)
throw new Error('Node is undefined');
- }
- if (node.isLeaf) {
- //console.log("found leaf node");
+
+ if (node.isLeaf)
return node;
- }
+
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 this.findLeafNode(closestEntry.node, point);
}
@@ -127,35 +115,11 @@ class MTree {
* @param {Node} node - The node to split
*/
splitNode(node) {
- const entries = node.entries;
- const halfSize = Math.floor(entries.length / 2);
+ const bestSplit = this.findBestSplit(node.entries);
- let minTotalRadius = Infinity;
- let bestSplit = null;
-
- // Loop through all combinations of entries that contain half of the total entries
- const entryCombinations = this.getCombinations(entries, halfSize);
- for (const leftEntries of entryCombinations) {
- 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);
- const totalRadius = leftRadius + rightRadius;
-
- // Update the best split if this one has a smaller radius
- if (totalRadius < minTotalRadius) {
- minTotalRadius = totalRadius;
- bestSplit = { leftEntries, rightEntries };
- }
- }
+ if (!bestSplit) return; // No valid split found
if (node.parent !== null) {
- // 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
const newNode = new Node(bestSplit.rightEntries, node.isLeaf, node.parent);
if (!node.isLeaf)
bestSplit.rightEntries.forEach(entry => entry.node.parent = newNode);
@@ -170,11 +134,9 @@ class MTree {
nodeEntry.updateRadius();
}
if (node.parent.entries.length > this.capacity)
- // If the parent node now has more than capacity entries, split it too
this.splitNode(node.parent);
} else {
- // If the node is a root node, split it into two new nodes
const leftNode = new Node(bestSplit.leftEntries, node.isLeaf);
const rightNode = new Node(bestSplit.rightEntries, node.isLeaf);
@@ -193,20 +155,67 @@ class MTree {
}
}
- // Helper function to generate all combinations of a given size
- getCombinations(arr, size) {
- if (size === 0) return [[]];
- const combinations = [];
- for (let i = 0; i < arr.length; i++) {
- const current = arr[i];
- const rest = arr.slice(i + 1);
- for (const combination of this.getCombinations(rest, size - 1)) {
- combinations.push([current, ...combination]);
+ findBestSplit(entries) {
+ const halfSize = Math.floor(entries.length / 2);
+ let minTotalRadius = Infinity;
+ let bestSplit = null;
+
+ // Use a heuristic approach to quickly find a good split
+ for (let i = 0; i < entries.length; i++) {
+ for (let j = i + 1; j < entries.length; j++) {
+ const leftEntries = [];
+ const rightEntries = [];
+
+ // Partition entries based on proximity to two chosen points
+ for (const entry of entries) {
+ if (this.distanceFunction(entry.point, entries[i].point) < this.distanceFunction(entry.point, entries[j].point)) {
+ leftEntries.push(entry);
+ } else {
+ rightEntries.push(entry);
+ }
+ }
+
+ if (leftEntries.length === halfSize) {
+ const leftRadius = this.calculateRadius(leftEntries);
+ const rightRadius = this.calculateRadius(rightEntries);
+ const totalRadius = leftRadius + rightRadius;
+
+ if (totalRadius < minTotalRadius) {
+ minTotalRadius = totalRadius;
+ bestSplit = { leftEntries, rightEntries };
+ }
+ }
}
}
- return combinations;
+
+ return bestSplit;
}
+ /**
+ * Gets all combinations of size 'size' from the given array 'arr'.
+ * @param {any[]} arr - The array to get combinations from
+ * @param {number} size - The size of the combinations to get
+ * @returns {any[][]} An array of combinations of the given size
+ */
+ /*getCombinations(arr, size) {
+ if (size === 0)
+ return [[]];
+ const result = [];
+ const helper = (offset, partialCombination) => {
+ if (partialCombination.length === size) {
+ result.push(partialCombination.slice());
+ return;
+ }
+ for (let i = offset; i <= arr.length - (size - partialCombination.length); i++) {
+ partialCombination.push(arr[i]);
+ helper(i + 1, partialCombination);
+ partialCombination.pop();
+ }
+ };
+ helper(0, []);
+ return result;
+ }*/
+
/**
* 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.
@@ -214,42 +223,34 @@ class MTree {
* @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 {Node} currentNode - 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
+ * @param {number} searchRadius - The radius to search within
+ * @param {number[][]} resultArray - An array to store the result in
*/
- 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);
- }
+ rangeQueryRecursive(currentNode, queryPoint, searchRadius, resultArray) {
+ if (currentNode.isLeaf) {
+ for (const entry of currentNode.entries) {
+ const distance = this.distanceFunction(queryPoint, entry.point);
+ if (distance <= searchRadius)
+ resultArray.push({ point: entry.point, distance: distance });
}
} else {
- for (const entry of node.entries) {
- //console.log(`rangeQueryRecursive: checking routing entry ${entry}`);
+ for (const entry of currentNode.entries) {
const distance = this.distanceFunction(queryPoint, entry.point);
- if (distance <= radius + entry.radius)
- this.rangeQueryRecursive(entry.node, queryPoint, radius, result);
- //else
- // console.log(`rangeQueryRecursive: skipping node ${entry.node.id} due to distance ${distance} > ${radius + entry.radius}`);
+ if (distance <= searchRadius + entry.radius)
+ this.rangeQueryRecursive(entry.node, queryPoint, searchRadius, resultArray);
}
}
}
-
/**
* Finds the k nearest neighbors to a given query point.
* @param {number[]} queryPoint - The point to find the nearest neighbors to
@@ -291,5 +292,3 @@ class MTree {
}
module.exports = MTree;
-
-
diff --git a/m-tree/nodes.js b/m-tree/nodes.js
index 31b1b24..415de39 100644
--- a/m-tree/nodes.js
+++ b/m-tree/nodes.js
@@ -24,14 +24,6 @@ class RoutingEntry {
return acc.map((val, i) => val + entry.point[i]);
}, new Array(this.mtree.dimension).fill(0));
this.point = sum.map(val => val / this.node.entries.length);
-
- /*const centroid = new Array(this.mtree.dimension).fill(0);
- for (const entry of this.node.entries) {
- for (let i = 0; i < this.mtree.dimension; i++) {
- centroid[i] += entry.point[i];
- }
- }
- this.point = centroid.map(x => x / this.node.entries.length);*/
}
/**
@@ -53,7 +45,16 @@ class RoutingEntry {
this.radius = Math.max(...allGroundEntries.map(entry => this.mtree.distanceFunction(entry.point, this.point)));
}
- // TODO check if new entry requires larger radius and then update
+ /**
+ * Checks if a new entry requires a larger radius and updates the radius if needed.
+ * @param {GroundEntry} newEntry - The new entry to check
+ */
+ updateRadiusIfNeeded(newEntry) {
+ const distance = this.mtree.distanceFunction(newEntry.point, this.point);
+ if (distance > this.radius) {
+ this.radius = distance;
+ }
+ }
}
class GroundEntry {
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..800e7ea
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+ MTree Interface
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/script/main.js b/public/script/main.js
new file mode 100644
index 0000000..638f6ae
--- /dev/null
+++ b/public/script/main.js
@@ -0,0 +1,220 @@
+let dimensions = 3;
+
+/**
+ * Checks if the MTree is initialized by sending a request to the server.
+ * Disables or enables the range query, k-nearest neighbors, and insert buttons
+ * based on the server's response.
+ * @return {Promise} A promise that resolves when the check is complete.
+ */
+async function checkIfTreeExists() {
+ const response = await fetch('/isInitialized');
+ const treeExists = await response.json();
+ toggleButtons(treeExists);
+}
+
+/**
+ * Enables or disables the range query, k-nearest neighbors, and insert buttons.
+ * @param {boolean} enable - Whether to enable (true) or disable (false) the buttons.
+ */
+function toggleButtons(enable) {
+ document.getElementById('rangeQueryButton').disabled = !enable;
+ document.getElementById('knnQueryButton').disabled = !enable;
+ document.getElementById('insertButton').disabled = !enable;
+ document.getElementById('saveTreeButton').disabled = !enable;
+}
+
+/**
+ * Performs a range query on the MTree.
+ * Retrieves all points in the tree that are within the given radius of the given query point.
+ * @return {Promise} A promise that resolves when the query is finished.
+ */
+async function performRangeQuery() {
+ const queryPointElement = document.getElementById('queryPoint');
+ const radiusElement = document.getElementById('radius');
+ const queryPoint = queryPointElement.value;
+ const radius = parseFloat(radiusElement.value);
+
+ document.getElementById('rangeQueryButton').disabled = true;
+ const response = await fetch(`/rangeQuery?queryPoint=${encodeURIComponent(queryPoint)}&radius=${encodeURIComponent(radius)}`);
+ setStatus(response.ok);
+ if (response.ok) {
+ const result = await response.json();
+ result.sort((a, b) => a.distance - b.distance);
+
+ const table = createTable(result);
+ document.getElementById('result').innerHTML = '';
+ document.getElementById('result').appendChild(table);
+ document.getElementById('status').innerText = 'Range query successful';
+ } else {
+ document.getElementById('status').innerText = `Error: ${await response.text()}`;
+ }
+
+ document.getElementById('rangeQueryButton').disabled = false;
+}
+
+/**
+ * Performs a K-Nearest Neighbors query on the MTree.
+ * @return {Promise} A promise that resolves when the query is finished.
+ */
+async function performKNNQuery() {
+ const queryPointElement = document.getElementById('queryPoint');
+ const kElement = document.getElementById('knn');
+ const queryPoint = queryPointElement.value;
+ const k = parseInt(kElement.value, 10);
+
+ const response = await fetch(`/kNNQuery?queryPoint=${encodeURIComponent(queryPoint)}&k=${encodeURIComponent(k)}`);
+ setStatus(response.ok);
+ if (response.ok) {
+ const result = await response.json();
+ const table = createTable(result);
+ document.getElementById('result').innerHTML = '';
+ document.getElementById('result').appendChild(table);
+ document.getElementById('status').innerText = 'KNN query successful';
+ } else {
+ document.getElementById('status').innerText = `Error: ${await response.text()}`;
+ }
+}
+
+/**
+ * Inserts a point into the MTree.
+ * After inserting the point, updates the placeholder value for the point input.
+ */
+async function insertPoint() {
+ document.getElementById('insertButton').disabled = true;
+
+ const pointString = document.getElementById('point').value;
+ const point = JSON.parse(pointString);
+
+ const response = await fetch('/insert', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ point })
+ });
+
+ setStatus(response.ok);
+ document.getElementById('status').innerText = response.ok ? 'Point inserted' : `Error: ${await response.text()}`;
+ document.getElementById('insertButton').disabled = false;
+}
+
+/**
+ * Creates an HTML table from the given result of a range or K-NN query.
+ * @param {Object[]} result - The result of the query.
+ * Each element of the array should be an object with a 'point' property (an array of numbers)
+ * and a 'distance' property (a number).
+ * @return {HTMLTableElement} The created table.
+ */
+function createTable(result) {
+ const table = document.createElement('table');
+ const headerRow = table.createTHead().insertRow();
+
+ const th1 = document.createElement('th');
+ th1.innerHTML = 'Point';
+ headerRow.appendChild(th1);
+
+ const th2 = document.createElement('th');
+ th2.innerHTML = 'Point';
+ headerRow.appendChild(th2);
+
+ result.forEach(row => {
+ const rowElement = table.insertRow();
+ rowElement.insertCell().innerText = `[${row.point.map(x => x.toFixed(5)).join(', ')}]`;
+ rowElement.insertCell().innerText = row.distance.toFixed(5);
+ });
+ return table;
+}
+
+/**
+ * Recreates the MTree with the given number of dimensions and points.
+ * After recreating the tree, updates the placeholder values for the query point and point inputs.
+ */
+async function recreateTree() {
+ const button = document.getElementById('recreateTreeButton')
+ button.disabled = true;
+ const dimensions = Number(document.getElementById('dimensions').value);
+ const pointCount = Number(document.getElementById('pointCount').value);
+ const response = await fetch('/recreate', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ dimensions, pointCount })
+ });
+ setStatus(response.ok);
+ document.getElementById('status').innerText = `${response.ok ? 'Tree recreated' : 'Error: ' + await response.text()}`;
+ if (response.ok) {
+ document.getElementById('queryPoint').placeholder = `e.g. [${Array(dimensions).fill(0.5).join(', ')}]`;
+ document.getElementById('point').placeholder = `e.g. [${Array(dimensions).fill(0.5).join(', ')}]`;
+ toggleButtons(true);
+ }
+ button.disabled = false;
+ button.textContent = 'Recreate Tree';
+}
+
+/**
+ * Saves the current MTree to a text area.
+ */
+async function saveTree() {
+ const response = await fetch('/tree');
+ setStatus(response.ok);
+ document.getElementById('status').innerText = response.ok ? 'Tree saved to file' : 'Error: ' + await response.text();
+ if (response.ok) {
+ const treeData = await response.text();
+ const blob = new Blob([treeData], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'tree.json';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }
+}
+
+/**
+ * Loads the MTree from the text area.
+ */
+async function loadTree() {
+ const input = document.getElementById('treeFileInput');
+ const file = input.files[0];
+ const reader = new FileReader();
+ reader.onload = async (event) => {
+ const treeData = event.target.result;
+ const response = await fetch('/loadTree', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: treeData
+ });
+
+ setStatus(response.ok);
+ if (response.ok) {
+ document.getElementById('status').innerText = 'Tree loaded';
+ toggleButtons(true);
+ } else {
+ document.getElementById('status').innerText = `Error: ${await response.text()}`;
+ }
+ };
+ reader.readAsText(file);
+}
+
+/**
+ * Sets the status element to display a success or failure message.
+ * @param {boolean} success - Whether the operation was successful.
+ */
+function setStatus(success) {
+ const statusElement = document.getElementById('status');
+ statusElement.classList.remove('error', 'success');
+ statusElement.classList.add(success ? 'success' : 'error');
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ checkIfTreeExists();
+ document.getElementById('rangeQueryButton').addEventListener('click', performRangeQuery);
+ document.getElementById('knnQueryButton').addEventListener('click', performKNNQuery);
+ document.getElementById('insertButton').addEventListener('click', insertPoint);
+ document.getElementById('recreateTreeButton').addEventListener('click', recreateTree);
+
+ document.getElementById('saveTreeButton').addEventListener('click', saveTree);
+ document.getElementById('loadTreeButton').addEventListener('click', loadTree);
+
+ document.getElementById('queryPoint').placeholder = `e.g. [${Array(dimensions).fill(0.5).join(', ')}]`;
+ document.getElementById('point').placeholder = `e.g. [${Array(dimensions).fill(0.5).join(', ')}]`;
+});
diff --git a/public/style/index.css b/public/style/index.css
new file mode 100644
index 0000000..7921eba
--- /dev/null
+++ b/public/style/index.css
@@ -0,0 +1,156 @@
+:root {
+ --background-color: #e9ecef;
+ --accent-color: #007bff;
+ --highlight-color: #0056b3;
+ --text-color: #6c757d;
+ --button-color: #007bff;
+ --button-highlight-color: #0056b3;
+ --section-color: #f8fafc;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: var(--background-color);
+}
+
+main {
+ padding: 1.5rem;
+}
+
+header {
+ display: flex;
+ justify-content: center;
+ background-color: var(--accent-color);
+ padding: 1rem;
+ box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.2);
+}
+
+h1 {
+ font-size: 2rem;
+ color: white;
+ margin: 0;
+ font-weight: bold;
+ text-align: center;
+}
+
+.form-group {
+ margin-bottom: 0.5rem;
+}
+
+label {
+ display: block;
+ margin-bottom: 0.5rem;
+ color: var(--text-color);
+}
+
+input[type="text"], input[type="number"] {
+ padding: 0.5rem;
+ width: 100%;
+ box-sizing: border-box;
+ border: 1px solid var(--text-color);
+ border-radius: 0.25rem;
+}
+
+button {
+ padding: 0.5rem 1rem;
+ background-color: var(--button-color);
+ color: white;
+ border: none;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ margin-bottom: 1rem;
+}
+
+button:hover {
+ background-color: var(--button-highlight-color);
+}
+
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+#result {
+ padding: 0.5rem;
+ border-radius: 0.25rem;
+}
+
+.column {
+ padding: 0.5rem;
+}
+
+#insertion-column {
+ width: 40%;
+}
+
+#queries-column {
+ width: 40%;
+}
+
+#save-load-column {
+ width: 20%;
+}
+
+section {
+ background-color: var(--section-color);
+ padding: 1rem;
+ border-radius: 0.25rem;
+ box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.2);
+}
+
+#column-section {
+ display: flex;
+ width: auto;
+ column-gap: 1rem;
+}
+
+#result-section {
+ margin-top: 1.5rem;
+}
+
+#result table {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+#result td, #result th {
+ padding: 0.8rem;
+}
+
+#result tr:nth-child(odd) {
+ background-color: #b3cdff;
+}
+
+#result tr:nth-child(even) {
+ background-color: #e7f0ff;
+}
+
+#result th {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ text-align: left;
+ background-color: var(--accent-color);
+ color: white;
+ font-weight: bold;
+}
+
+#status-section {
+ margin-top: 1.5rem;
+ padding: 1rem 1.5rem;
+}
+
+#status {
+ padding: 0.5rem;
+}
+
+#status.success {
+ background-color: #28a745;
+ color: white;
+}
+
+#status.error {
+ background-color: #ff6c6c;
+ color: white;
+}