diff --git a/index.js b/index.js index 21f9c07..61fcc15 100644 --- a/index.js +++ b/index.js @@ -1,18 +1,15 @@ const express = require('express'); const bodyParser = require('body-parser'); const MTree = require('./m-tree/mtree'); +const Node = require('./m-tree/nodes').Node; +const distanceFunctions = require('./m-tree/distance-functions'); // Generator used to generate random points for the MTree const Generator = require('./data/generator'); // Express app const app = express(); -app.use(bodyParser.json()); - -// Euclidean distance function used as the distance metric for the MTree -function euclideanDistance(a, b) { - return Math.sqrt(a.reduce((acc, val, i) => acc + (val - b[i]) ** 2, 0)); -} +app.use(bodyParser.json({ limit: '100mb' })); // Create an MTree with the given dimensions and capacity, using the Euclidean distance let mtree = null; @@ -28,15 +25,20 @@ 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) => { + const tree = JSON.parse(JSON.stringify(mtree, (key, value) => { if (key === 'parent' || key === 'mtree') return value && value.id; return value; - }))); + })); + const distanceFunctionName = Object.keys(distanceFunctions).find(key => distanceFunctions[key] === mtree.distanceFunction); + tree.distanceFunctionName = distanceFunctionName; + res.send(tree); }); // Endpoint to check if the MTree has been initialized app.get('/isInitialized', (req, res) => { res.send(!!mtree); }); +// Return the list of supported distance functions +app.get('/distanceFunctions', (req, res) => { res.send(Object.keys(distanceFunctions)); }); // Insert a point into the MTree app.post('/insert', (req, res) => { @@ -76,7 +78,7 @@ app.get('/rangeQuery', (req, res) => { //console.log(result.length); console.time('sequentialSearch'); - const sequentialResult = points.filter(point => euclideanDistance(point, parsedQueryPoint) <= parsedRadius); + const sequentialResult = points.filter(point => mtree.distanceFunction(point, parsedQueryPoint) <= parsedRadius); console.timeEnd('sequentialSearch'); //console.log(sequentialResult.length); @@ -106,8 +108,9 @@ app.get('/kNNQuery', (req, res) => { //console.log(result); console.time('sequentialSearch'); - const sequentialResult = points.sort((a, b) => euclideanDistance(a, parsedQueryPoint) - euclideanDistance(b, parsedQueryPoint)).slice(0, kInt); + const sequentialResult = points.sort((a, b) => mtree.distanceFunction(a, parsedQueryPoint) - mtree.distanceFunction(b, parsedQueryPoint)).slice(0, kInt); console.timeEnd('sequentialSearch'); + //const sequentialResultWithDistance = sequentialResult.map(point => ({point, distance: mtree.distanceFunction(point, parsedQueryPoint)})); //console.log(sequentialResult); res.send(result); @@ -115,31 +118,57 @@ app.get('/kNNQuery', (req, res) => { // Recreate the MTree with the given dimensions app.post('/recreate', (req, res) => { - let { dimensions, pointCount } = req.body; + let { dimensions, pointCount, capacity, distanceFunction } = req.body; dimensions = parseInt(dimensions); pointCount = parseInt(pointCount); - console.log(dimensions, pointCount); + capacity = parseInt(capacity); + console.log(dimensions, pointCount, capacity, distanceFunction); 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'); + if (isNaN(capacity) || capacity <= 0) + return res.status(400).send('Invalid capacity'); + + const distanceFunctionName = distanceFunction.toLowerCase(); + const chosenDistanceFunction = distanceFunctions[distanceFunctionName]; + if (!chosenDistanceFunction) + return res.status(400).send(`Invalid distance function. Supported functions: ${Object.keys(distanceFunctions).join(', ')}`); const generator = new Generator(dimensions); - mtree = new MTree(dimensions, 10); + mtree = new MTree(dimensions, capacity, chosenDistanceFunction); points = generator.generateMany(pointCount); // points.forEach(point => mtree.insert(point)); + console.time('tree creation') points.forEach((point, i) => { - if (i % 100 === 0) { - console.time(`insertion of next 100 points`); + if (i % 1000 === 0) { + console.time(`insertion of next 1000 points`); console.log(`inserted ${i} points`); } mtree.insert(point); - if (i % 100 === 99) - console.timeEnd(`insertion of next 100 points`); + if (i % 1000 === 999) + console.timeEnd(`insertion of next 1000 points`); }) - + res.send('MTree recreated'); + console.timeEnd('tree creation') +}); + +// Load the MTree from the posted JSON +app.post('/loadTree', (req, res) => { + const tree = req.body; + if (!tree) + return res.status(400).send('Missing tree'); + try { + mtree = MTree.fromJSON(tree); + } catch (e) { + return res.status(400).send('Invalid tree'); + } + + points = Node.findGroundEntries(tree.root).map(entry => entry.point); + + res.send('MTree loaded'); }); // Start the server diff --git a/m-tree/distance-functions.js b/m-tree/distance-functions.js new file mode 100644 index 0000000..efc6b14 --- /dev/null +++ b/m-tree/distance-functions.js @@ -0,0 +1,82 @@ +/** + * Calculates the Euclidean distance between two points in n-dimensional space. + * + * @param {number[]} a - The first point, represented as an array of numbers. + * @param {number[]} b - The second point, represented as an array of numbers. + * @returns {number} The Euclidean distance between the two points. + */ +function euclideanDistance(a, b) { + return Math.sqrt(a.reduce((acc, val, i) => acc + (val - b[i]) ** 2, 0)); +} + +/** + * Calculates the Manhattan distance (L1) between two points in n-dimensional space. + * + * @param {number[]} a - The first point, represented as an array of numbers. + * @param {number[]} b - The second point, represented as an array of numbers. + * @returns {number} The Manhattan distance between the two points. + */ +function manhattanDistance(a, b) { + return a.reduce((acc, val, i) => acc + Math.abs(val - b[i]), 0); +} + +/** + * Calculates the Maximum distance (L∞) between two points in n-dimensional space. + * + * @param {number[]} a - The first point, represented as an array of numbers. + * @param {number[]} b - The second point, represented as an array of numbers. + * @returns {number} The Maximum distance between the two points. + */ +function maximumDistance(a, b) { + return Math.max(...a.map((val, i) => Math.abs(val - b[i]))); +} + +/** + * Calculates the Minkowski distance (Lp) between two points in n-dimensional space. + * + * @param {number[]} a - The first point, represented as an array of numbers. + * @param {number[]} b - The second point, represented as an array of numbers. + * @param {number} p - The order of the Minkowski distance. + * @returns {number} The Minkowski distance between the two points. + */ +function minkowskiDistance(a, b, p) { + return Math.pow(a.reduce((acc, val, i) => acc + Math.pow(Math.abs(val - b[i]), p), 0), 1 / p); +} + +/** + * Calculates the quadratic distance between two points in n-dimensional space. + * + * @param {number[]} a - The first point, represented as an array of numbers. + * @param {number[]} b - The second point, represented as an array of numbers. + * @returns {number} The quadratic distance between the two points. + */ +function quadraticDistance(a, b) { + return a.reduce((acc, val, i) => acc + (val - b[i]) ** 2, 0); +} + +/** + * Calculates the angle between two points in n-dimensional space. + * + * @param {number[]} a - The first point, represented as an array of numbers. + * @param {number[]} b - The second point, represented as an array of numbers. + * @returns {number} The angle between the two points in radians. + */ +function angleDistance(a, b) { + const dotProduct = a.reduce((acc, val, i) => acc + val * b[i], 0); + const magnitudeA = Math.sqrt(a.reduce((acc, val) => acc + val ** 2, 0)); + const magnitudeB = Math.sqrt(b.reduce((acc, val) => acc + val ** 2, 0)); + + return Math.acos(dotProduct / (magnitudeA * magnitudeB)); +} + +const distanceFunctions = { + 'euclidean': euclideanDistance, + 'manhattan': manhattanDistance, + 'maximum': maximumDistance, + 'minkowski': minkowskiDistance, + 'quadratic': quadraticDistance, + 'angle': angleDistance +}; + +//export { euclideanDistance, manhattanDistance, maximumDistance, minkowskiDistance, quadraticDistance, angleDistance }; +module.exports = distanceFunctions; diff --git a/m-tree/mtree.js b/m-tree/mtree.js index c57f622..32af549 100644 --- a/m-tree/mtree.js +++ b/m-tree/mtree.js @@ -1,4 +1,6 @@ -const { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry } = require('./nodes'); +const { Node, GroundEntry } = require('./nodes'); +const calculateCentroid = require("./utils").calculateCentroid; +const distanceFunctions = require('./distance-functions'); class MTree { /** @@ -11,7 +13,36 @@ class MTree { this.dimension = dimension; this.capacity = capacity; this.distanceFunction = distanceFunction; - this.root = new LeafNode([], null); + this.root = new Node([], true, null, this.distanceFunction); + } + + + /** + * Constructs a new MTree from a given JSON representation of the MTree. + * @param {Object} tree - The JSON representation of the MTree + * @returns {MTree} A new MTree instance + */ + static fromJSON(tree) { + const mtree = new MTree(tree.dimension, tree.capacity, distanceFunctions[tree.distanceFunctionName]); + console.log(tree); + + function assignParents(node, parent) { + node.parent = parent; + Object.setPrototypeOf(node, Node.prototype); + for (const entry of node.entries) { + if (entry.entries) { + assignParents(entry, node); + } + else { + Object.setPrototypeOf(entry, GroundEntry.prototype); + } + } + } + assignParents(tree.root, null); + + mtree.root = tree.root; + + return mtree; } /** @@ -29,22 +60,17 @@ class MTree { const groundEntry = new GroundEntry(point); // Add the ground entry to the leaf node's entries - leafNode.entries.push(groundEntry); + leafNode.insert(groundEntry, this.distanceFunction); // If the leaf node now has more than the capacity amount of entries, split it 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 let currentNode = leafNode; while (currentNode.parent !== null) { - const routingEntry = currentNode.parent.entries.find(entry => entry.node === currentNode); - if (routingEntry) { - //console.log(`updating routing entry: ${routingEntry.node.id}`); - routingEntry.updateCentroid(); - routingEntry.updateRadius(); - } + currentNode.parent.updateCentroid(this.distanceFunction); + currentNode.parent.updateRadiusIfNeeded(groundEntry, this.distanceFunction); currentNode = currentNode.parent; } } @@ -59,15 +85,14 @@ class MTree { 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); + + return this.findLeafNode(closestEntry, point); } /** @@ -98,14 +123,11 @@ class MTree { * @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(this.dimension).fill(0)); - const centroidPoint = sum.map(val => val / entries.length); + const centroidPoint = calculateCentroid(entries, this.distanceFunction); - // 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))); + // Calculate the maximum distance from the centroid to any point in the entries, + // taking into account the radius of each node + const maxDistance = Math.max(...entries.map(entry => this.distanceFunction(entry.point, centroidPoint)/* + entry.radius*/)); return maxDistance; } @@ -120,62 +142,74 @@ class MTree { if (!bestSplit) return; // No valid split found if (node.parent !== null) { - const newNode = new Node(bestSplit.rightEntries, node.isLeaf, node.parent); - if (!node.isLeaf) - bestSplit.rightEntries.forEach(entry => entry.node.parent = newNode); + const newNode = new Node(bestSplit.leftEntries, node.isLeaf, node.parent, this.distanceFunction); - const rightEntry = new RoutingEntry(newNode, this); node.entries = bestSplit.leftEntries; - node.parent.entries.push(rightEntry); + node.parent.insert(newNode, this.distanceFunction); - const nodeEntry = node.parent.entries.find(entry => entry.node === node); - if (nodeEntry) { - nodeEntry.updateCentroid(); - nodeEntry.updateRadius(); - } if (node.parent.entries.length > this.capacity) this.splitNode(node.parent); - } else { - 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); - } + node.entries = bestSplit.rightEntries; + node.updateCentroid(this.distanceFunction); + node.updateRadius(this.distanceFunction); - const rightEntry = new RoutingEntry(rightNode, this); - const leftEntry = new RoutingEntry(leftNode, this); + } else { + const leftNode = new Node(bestSplit.leftEntries, node.isLeaf, node, this.distanceFunction); + const rightNode = new Node(bestSplit.rightEntries, node.isLeaf, node, this.distanceFunction); - node.entries = [leftEntry, rightEntry]; + node.entries = [leftNode, rightNode]; node.isLeaf = false; } } + /** + * Finds the best split for a node's entries to minimize the total radius of two resulting nodes. + * Uses a heuristic approach by iterating through pairs of entries and partitioning the rest + * based on proximity to these chosen points. Calculates total radius from the centroid of entries + * and selects the partition with the smallest total radius. + * + * @param {Object[]} entries - The array of node entries to split. + * @returns {Object|null} An object containing `leftEntries` and `rightEntries` arrays representing + * the best split or null if no valid split is found. + */ findBestSplit(entries) { + if (entries.length < 2) return null; + 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++) { + // Precompute distances to reduce redundant calculations + const distances = entries.map((entry, index) => + entries.map((otherEntry, otherIndex) => + index !== otherIndex ? this.distanceFunction(entry.point, otherEntry.point) : Infinity + ) + ); + + for (let i = 0; i < entries.length - 1; 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); + for (let k = 0; k < entries.length; k++) { + if (k === i || k === j) continue; + const distanceI = distances[k][i]; + const distanceJ = distances[k][j]; + + if (distanceI < distanceJ) { + leftEntries.push(entries[k]); } else { - rightEntries.push(entry); + rightEntries.push(entries[k]); } } - if (leftEntries.length === halfSize) { + // Ensure both chosen points are included in their respective partitions + leftEntries.push(entries[i]); + rightEntries.push(entries[j]); + + if (Math.abs(leftEntries.length - rightEntries.length) <= 1) { const leftRadius = this.calculateRadius(leftEntries); const rightRadius = this.calculateRadius(rightEntries); const totalRadius = leftRadius + rightRadius; @@ -246,7 +280,7 @@ class MTree { for (const entry of currentNode.entries) { const distance = this.distanceFunction(queryPoint, entry.point); if (distance <= searchRadius + entry.radius) - this.rangeQueryRecursive(entry.node, queryPoint, searchRadius, resultArray); + this.rangeQueryRecursive(entry, queryPoint, searchRadius, resultArray); } } } @@ -284,7 +318,7 @@ class MTree { 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); + this.kNNQueryRecursive(entry, queryPoint, k, result); } } } @@ -292,3 +326,4 @@ class MTree { } module.exports = MTree; + diff --git a/m-tree/nodes.js b/m-tree/nodes.js index 415de39..c446fa7 100644 --- a/m-tree/nodes.js +++ b/m-tree/nodes.js @@ -1,61 +1,5 @@ const MTree = require("./mtree"); - -class RoutingEntry { - /** - * Creates a new routing entry. - * Initializes the routing entry with the given node and dimension, - * and computes the initial centroid and radius for the node's entries. - * @param {Node} node - The node associated with the entry - * @param {MTree} mtree - The number of dimensions for the entry - */ - constructor(node, mtree) { - this.node = node; - this.mtree = mtree; - this.updateCentroid(); - this.updateRadius(); - } - - /** - * Updates the centroid of the routing entry. - * Computes the average position of all points in the associated node's entries. - */ - updateCentroid() { - const sum = this.node.entries.reduce((acc, entry) => { - 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); - } - - /** - * Updates the radius of the routing entry. - * Determines the maximum distance from the centroid to any point in the node's entries. - */ - updateRadius() { - const allGroundEntries = []; - const findGroundEntries = node => { - if (node.isLeaf) { - allGroundEntries.push(...node.entries.filter(entry => entry instanceof GroundEntry)); - } else { - for (const entry of node.entries) { - findGroundEntries(entry.node); - } - } - }; - findGroundEntries(this.node); - this.radius = Math.max(...allGroundEntries.map(entry => this.mtree.distanceFunction(entry.point, this.point))); - } - - /** - * 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; - } - } -} +const calculateCentroid = require("./utils").calculateCentroid; class GroundEntry { /** @@ -72,40 +16,89 @@ class GroundEntry { 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) { + +/** + * Constructs a new Node instance. + * @param {Node[]|GroundEntry[]} entries - The node entries, either GroundEntries or other Nodes. + * @param {boolean} isLeaf - Indicates if the node is a leaf node. + * @param {Node|null} parent - The parent node, or null if this is the root node. + * @param {function} distanceFunction - The function used to calculate distances between points. + */ + constructor(entries, isLeaf, parent, distanceFunction) { this.id = Node.idCounter++; // Assign a unique ID to the node this.entries = entries; this.isLeaf = isLeaf; this.parent = parent; + this.updateCentroid(distanceFunction); + this.updateRadius(distanceFunction); + + if (!isLeaf) + this.entries.forEach(entry => entry.parent = this); } -} -class LeafNode extends Node { /** - * Create a new leaf node - * @param {GroundEntry[]} entries - The entries in the node - * @param {Node} parent - The parent node + * Inserts a new entry into the node. + * Updates the centroid and the radius of the node if needed. + * + * @param {GroundEntry|Node} entry - The entry to insert into the node. + * @param {function} distanceFunction - The distance function used for updating the centroid. */ - constructor(entries, parent) { - super(entries, true, parent); // Call the Node constructor with isLeaf set to true + insert(entry, distanceFunction) { + this.entries.push(entry); + this.updateCentroid(distanceFunction); + this.updateRadiusIfNeeded(entry, distanceFunction); } -} -class InternalNode extends Node { /** - * Create a new internal node - * @param {RoutingEntry[]} entries - The entries in the node - * @param {Node} parent - The parent node + * Calculates the centroid of the node's entries using the given distance function. + * Sets the calculated centroid as the point of the node. + * @param {function} distanceFunction - The distance function to use for centroid calculation. */ - constructor(entries, parent) { - super(entries, false, parent); // Call the Node constructor with isLeaf set to false + updateCentroid(distanceFunction) { + this.point = calculateCentroid(this.entries, distanceFunction); + } + + /** + * Updates the radius of the node by calculating the maximum distance from the node's centroid + * to any of its ground entries, using the provided distance function. + * + * @param {function} distanceFunction - The function used to calculate the distance between two points. + */ + updateRadius(distanceFunction) { + const groundEntries = Node.findGroundEntries(this); + this.radius = Math.max(...groundEntries.map(entry => distanceFunction(entry.point, this.point))); + } + + /** + * Finds and returns all ground entries within the given node. + * Recursively traverses the node's entries, filtering and collecting all instances of GroundEntry. + * @param {Node} node - The node to start searching from. + * @returns {GroundEntry[]} An array of ground entries found within the node. + */ + static findGroundEntries(node) { + if (node.isLeaf) { + return node.entries.filter(entry => entry instanceof GroundEntry); + } else { + const result = []; + for (const entry of node.entries) { + result.push(...Node.findGroundEntries(entry)); + } + return result; + } + }; + + /** + * Updates the radius of the node if the distance from the node's centroid + * to the new entry is greater than the current radius. + * @param {GroundEntry} newEntry - The new ground entry to check against. + * @param {function} distanceFunction - The function used to calculate the distance between two points. + */ + updateRadiusIfNeeded(newEntry, distanceFunction) { + const distance = distanceFunction(newEntry.point, this.point); + if (distance > this.radius) { + this.radius = distance; + } } } -module.exports = { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry }; +module.exports = { Node, GroundEntry }; diff --git a/m-tree/utils.js b/m-tree/utils.js new file mode 100644 index 0000000..01d6cba --- /dev/null +++ b/m-tree/utils.js @@ -0,0 +1,29 @@ +/** + * Calculates the centroid of an array of entries. + * The centroid is the point that is the mean of all the points in the array. + * The mean is calculated using the weight of each point, which defaults to 1. + * @param {Object[]} entries - The array of entries to calculate the centroid from. + * @param {function} [distanceFunction] - A distance function to use to calculate the weight of each point. + * @returns {number[]} The centroid of the array of entries. + */ +function calculateCentroid(entries, distanceFunction) { +if (entries.length === 0) + return; + + const length = entries[0].point.length; + const sum = new Array(length).fill(0); + let totalWeight = 0; + + for (const entry of entries) { + const weight = entry.radius || 1; + //const weight = distanceFunction(entry.point, this.point); + for (let i = 0; i < length; i++) { + sum[i] += entry.point[i] * weight; + } + totalWeight += weight; + } + + return sum.map(val => val / totalWeight); +} + +module.exports = { calculateCentroid }; \ No newline at end of file diff --git a/public/index.html b/public/index.html index 800e7ea..33997c7 100644 --- a/public/index.html +++ b/public/index.html @@ -31,8 +31,18 @@ +
+ + +
+
+ + +
+
@@ -52,6 +62,7 @@
+
diff --git a/public/script/main.js b/public/script/main.js index 638f6ae..f9d6cd5 100644 --- a/public/script/main.js +++ b/public/script/main.js @@ -112,7 +112,7 @@ function createTable(result) { headerRow.appendChild(th1); const th2 = document.createElement('th'); - th2.innerHTML = 'Point'; + th2.innerHTML = 'Distance'; headerRow.appendChild(th2); result.forEach(row => { @@ -132,10 +132,12 @@ async function recreateTree() { button.disabled = true; const dimensions = Number(document.getElementById('dimensions').value); const pointCount = Number(document.getElementById('pointCount').value); + const capacity = Number(document.getElementById('capacity').value); + const distanceFunction = document.getElementById('distanceFunction').value; const response = await fetch('/recreate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ dimensions, pointCount }) + body: JSON.stringify({ dimensions, pointCount, capacity, distanceFunction }) }); setStatus(response.ok); document.getElementById('status').innerText = `${response.ok ? 'Tree recreated' : 'Error: ' + await response.text()}`; @@ -161,7 +163,7 @@ async function saveTree() { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'tree.json'; + a.download = 'mtree.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); @@ -172,9 +174,40 @@ async function saveTree() { /** * 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); +}*/ + +/** + * Loads the MTree from the selected file and posts it to the loadTree API endpoint. + */ async function loadTree() { const input = document.getElementById('treeFileInput'); const file = input.files[0]; + if (!file) { + setStatus(false); + document.getElementById('status').innerText = 'Error: No file selected'; + return; + } const reader = new FileReader(); reader.onload = async (event) => { const treeData = event.target.result; @@ -217,4 +250,16 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('queryPoint').placeholder = `e.g. [${Array(dimensions).fill(0.5).join(', ')}]`; document.getElementById('point').placeholder = `e.g. [${Array(dimensions).fill(0.5).join(', ')}]`; + + const distanceFunctionSelect = document.getElementById('distanceFunction'); + fetch('/distanceFunctions') + .then(response => response.json()) + .then(distanceFunctions => { + distanceFunctions.forEach(distanceFunction => { + const option = document.createElement('option'); + option.value = distanceFunction; + option.innerText = distanceFunction; + distanceFunctionSelect.appendChild(option); + }); + }); }); diff --git a/public/style/index.css b/public/style/index.css index 7921eba..a144aa0 100644 --- a/public/style/index.css +++ b/public/style/index.css @@ -53,6 +53,26 @@ input[type="text"], input[type="number"] { border-radius: 0.25rem; } +input[type="file"] { + padding: 0.5rem; + width: 100%; + box-sizing: border-box; + border: 1px solid var(--text-color); + border-radius: 0.25rem; +} + +select { + padding: 0.5rem; + width: 100%; + box-sizing: border-box; + border: 1px solid var(--text-color); + border-radius: 0.25rem; + background-color: white; + color: var(--text-color); + appearance: none; + cursor: pointer; +} + button { padding: 0.5rem 1rem; background-color: var(--button-color);