Created user interface

main
František Špaček 1 year ago
parent 8481ab84a1
commit 11e4a67708

@ -1,83 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MTree API Interface</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
h1 {
color: #333;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"] {
padding: 8px;
width: 100%;
box-sizing: border-box;
}
button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<h1>MTree API Interface</h1>
<div class="form-group">
<label for="queryPoint">Query Point (JSON Array):</label>
<input type="text" id="queryPoint" placeholder='e.g. [0.5, 0.5]' />
</div>
<div class="form-group">
<label for="radius">Radius:</label>
<input type="text" id="radius" placeholder="e.g. 1.0" />
</div>
<button onclick="performRangeQuery()">Perform Range Query</button>
<div class="form-group">
<label for="knn">Number of Neighbors (k):</label>
<input type="text" id="knn" placeholder="e.g. 5" />
</div>
<button onclick="performKNNQuery()">Perform KNN Query</button>
<div id="result"></div>
<script>
async function performRangeQuery() {
const queryPoint = document.getElementById('queryPoint').value;
const radius = document.getElementById('radius').value;
const response = await fetch(`/rangeQuery?queryPoint=${encodeURIComponent(queryPoint)}&radius=${encodeURIComponent(radius)}`);
document.getElementById('result').innerText = await response.text();
}
async function performKNNQuery() {
const queryPoint = document.getElementById('queryPoint').value;
const k = document.getElementById('knn').value;
const response = await fetch(`/kNNQuery?queryPoint=${encodeURIComponent(queryPoint)}&k=${encodeURIComponent(k)}`);
document.getElementById('result').innerText = await response.text();
}
</script>
</body>
</html>

@ -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'); });

@ -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;

@ -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 {

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MTree Interface</title>
<link rel="preload" href="style/index.css" as="style">
<link rel="stylesheet" href="style/index.css">
<script src="script/main.js" defer></script>
</head>
<body>
<header>
<h1>MTree Interface</h1>
</header>
<main>
<section id="column-section">
<div class="column" id="insertion-column">
<div class="form-group">
<label for="point">Point to Insert:</label>
<input type="text" id="point" />
</div>
<button id="insertButton" disabled>Insert Point</button>
<div class="form-group">
<label for="dimensions">Dimensions:</label>
<input type="number" id="dimensions" value="3" placeholder="e.g. 3" min="0" />
</div>
<div class="form-group">
<label for="pointCount">Number of Points:</label>
<input type="number" id="pointCount" value="1000" placeholder="e.g. 1000" min="0" />
</div>
<button id="recreateTreeButton">Initialize Tree</button>
</div>
<div class="column" id="queries-column">
<div class="form-group">
<label for="queryPoint">Query Point:</label>
<input type="text" id="queryPoint" />
</div>
<div class="form-group">
<label for="radius">Radius:</label>
<input type="number" id="radius" placeholder="e.g. 1.0" min="0" step="0.01" />
</div>
<button id="rangeQueryButton" disabled>Perform Range Query</button>
<div class="form-group">
<label for="knn">Number of Neighbors (k):</label>
<input type="number" id="knn" placeholder="e.g. 5" min="0" />
</div>
<button id="knnQueryButton" disabled>Perform KNN Query</button>
</div>
<div class="column" id="save-load-column">
<button id="saveTreeButton" disabled>Save Tree</button>
<button id="loadTreeButton">Load Tree</button>
</div>
</section>
<section id="status-section">
<div id="status">
</div>
</section>
<section id="result-section">
<div id="result">
</div>
</section>
</main>
</body>
</html>

@ -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<void>} 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<void>} 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<void>} 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(', ')}]`;
});

@ -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;
}
Loading…
Cancel
Save

Powered by TurnKey Linux.