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

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