All basic function implemented

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

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

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

@ -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,7 +85,6 @@ class MTree {
findLeafNode(node, point) {
if (!node)
throw new Error('Node is undefined');
if (node.isLeaf)
return node;
@ -67,7 +92,7 @@ class MTree {
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;

@ -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
* 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) {
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);
}
/**
* 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.
*/
insert(entry, distanceFunction) {
this.entries.push(entry);
this.updateCentroid(distanceFunction);
this.updateRadiusIfNeeded(entry, distanceFunction);
}
class LeafNode extends Node {
/**
* Create a new leaf node
* @param {GroundEntry[]} 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, true, parent); // Call the Node constructor with isLeaf set to true
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)));
}
class InternalNode extends Node {
/**
* Create a new internal node
* @param {RoutingEntry[]} entries - The entries in the node
* @param {Node} parent - The parent node
* 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.
*/
constructor(entries, parent) {
super(entries, false, parent); // Call the Node constructor with isLeaf set to false
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 };

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

@ -31,8 +31,18 @@
<label for="pointCount">Number of Points:</label>
<input type="number" id="pointCount" value="1000" placeholder="e.g. 1000" min="0" />
</div>
<div class="form-group">
<label for="capacity">Node Capacity:</label>
<input type="number" id="capacity" value="10" placeholder="e.g. 10" min="1" />
</div>
<div class="form-group">
<label for="distanceFunction">Distance Function:</label>
<select id="distanceFunction">
</select>
</div>
<button id="recreateTreeButton">Initialize Tree</button>
</div>
<div class="column" id="queries-column">
<div class="form-group">
<label for="queryPoint">Query Point:</label>
@ -52,6 +62,7 @@
</div>
<div class="column" id="save-load-column">
<button id="saveTreeButton" disabled>Save Tree</button>
<input type="file" id="treeFileInput" accept=".json" />
<button id="loadTreeButton">Load Tree</button>
</div>
</section>

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

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

Loading…
Cancel
Save

Powered by TurnKey Linux.