mirror of
https://github.com/ceratic/project_vollidioten_website.git
synced 2026-05-14 00:16:47 +02:00
feat: add world map functionality and admin map management
- Added world map page with interactive marker display - Implemented admin map management for marker CRUD operations - Added map layers and markers seed data to database - Integrated new routes for map functionality - Updated database configuration for production environment - Added documentation page route - Enhanced package.json with required dependencies for map features
This commit is contained in:
@@ -2,9 +2,9 @@ const mysql = require('mysql2');
|
||||
|
||||
// Database Config from Env
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASS || '',
|
||||
host: process.env.DB_HOST || '192.168.1.102',
|
||||
user: process.env.DB_USER || 'obsidian_user',
|
||||
password: process.env.DB_PASS || 'obsidian_pass',
|
||||
database: process.env.DB_NAME || 'obsidian_db',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
@@ -90,6 +90,41 @@ const SEED_PROJECTS = [
|
||||
}
|
||||
];
|
||||
|
||||
const SEED_MAP_LAYERS = [
|
||||
{ id: 'layer-1', name: 'Städte', description: 'Alle Städte und Siedlungen', order_index: 1 },
|
||||
{ id: 'layer-2', name: 'Points of Interest', description: 'Besondere Orte und Sehenswürdigkeiten', order_index: 2 },
|
||||
{ id: 'layer-3', name: 'Spieler-Häuser', description: 'Spieler-Wohnsitze und Häuser', order_index: 3 }
|
||||
];
|
||||
|
||||
const SEED_MAP_MARKERS = [
|
||||
{
|
||||
id: 'marker-1',
|
||||
name: 'Provisorium Null',
|
||||
type: 'city',
|
||||
x_coord: -2560,
|
||||
z_coord: 512,
|
||||
description: 'Die erste Siedlung der neuen Ära',
|
||||
linked_entity_type: 'organization',
|
||||
linked_entity_id: 'org-3',
|
||||
icon_type: 'city',
|
||||
color: '#2563eb',
|
||||
is_public: 1
|
||||
},
|
||||
{
|
||||
id: 'marker-2',
|
||||
name: 'Sakura',
|
||||
type: 'city',
|
||||
x_coord: 1536,
|
||||
z_coord: -512,
|
||||
description: 'Eine dunkle, biolumineszente Hafenstadt',
|
||||
linked_entity_type: 'organization',
|
||||
linked_entity_id: 'org-4',
|
||||
icon_type: 'city',
|
||||
color: '#dc2626',
|
||||
is_public: 1
|
||||
}
|
||||
];
|
||||
|
||||
// Retry connection logic for Docker
|
||||
function init() {
|
||||
const tryConnect = () => {
|
||||
@@ -169,6 +204,34 @@ function setupTables() {
|
||||
logoImageId VARCHAR(50),
|
||||
shopCatalog JSON,
|
||||
gallery JSON
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS map_markers (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
type VARCHAR(50), -- 'city', 'poi', 'player_home', 'waypoint'
|
||||
x_coord INTEGER,
|
||||
z_coord INTEGER,
|
||||
description TEXT,
|
||||
linked_entity_type VARCHAR(50), -- 'city', 'organization', 'player'
|
||||
linked_entity_id VARCHAR(50),
|
||||
icon_type VARCHAR(50), -- 'city', 'house', 'chest', 'flag', etc.
|
||||
color VARCHAR(7), -- hex color code
|
||||
is_public TINYINT DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS map_layers (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
description TEXT,
|
||||
is_active TINYINT DEFAULT 1,
|
||||
order_index INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS map_metadata (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)`
|
||||
];
|
||||
|
||||
@@ -212,6 +275,28 @@ function seedData() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Seed map layers
|
||||
pool.query("SELECT COUNT(*) as count FROM map_layers", (err, rows) => {
|
||||
if (!err && rows[0].count === 0) {
|
||||
console.log("Seeding Map Layers...");
|
||||
SEED_MAP_LAYERS.forEach(layer => {
|
||||
pool.query("INSERT INTO map_layers VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[layer.id, layer.name, layer.description, 1, layer.order_index, new Date().toISOString().slice(0, 19).replace('T', ' ')]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Seed map markers
|
||||
pool.query("SELECT COUNT(*) as count FROM map_markers", (err, rows) => {
|
||||
if (!err && rows[0].count === 0) {
|
||||
console.log("Seeding Map Markers...");
|
||||
SEED_MAP_MARKERS.forEach(marker => {
|
||||
pool.query("INSERT INTO map_markers (id, name, type, x_coord, z_coord, description, linked_entity_type, linked_entity_id, icon_type, color, is_public) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[marker.id, marker.name, marker.type, marker.x_coord, marker.z_coord, marker.description, marker.linked_entity_type, marker.linked_entity_id, marker.icon_type, marker.color, marker.is_public]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wrapper to mimic SQLite API for easy migration in server.js
|
||||
|
||||
52
backend/debug-map-detailed.js
Normal file
52
backend/debug-map-detailed.js
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Detailed debug script for map processing
|
||||
const debugInterface = require('./server.js');
|
||||
const { db } = require('./database');
|
||||
|
||||
console.log('🔧 Detailed Map Processor Debug Console');
|
||||
console.log('=====================================');
|
||||
|
||||
async function runDetailedDebug() {
|
||||
try {
|
||||
console.log('1. Testing database connection...');
|
||||
const dbTest = await new Promise((resolve, reject) => {
|
||||
db.get("SELECT 1 as test", [], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
console.log('✅ Database connection successful:', dbTest);
|
||||
|
||||
console.log('\n2. Testing tile discovery...');
|
||||
const tiles = await debugInterface.getTiles();
|
||||
console.log('✅ Tile discovery completed');
|
||||
|
||||
console.log('\n3. Testing map metadata...');
|
||||
const metadata = await debugInterface.getMetadata();
|
||||
console.log('✅ Metadata retrieval completed');
|
||||
|
||||
console.log('\n4. Testing coordinate conversion...');
|
||||
await debugInterface.testCoords(0, 0);
|
||||
await debugInterface.testCoords(1000, 1000);
|
||||
console.log('✅ Coordinate conversion completed');
|
||||
|
||||
console.log('\n5. Attempting map assembly...');
|
||||
await debugInterface.assembleMap();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Detailed Error Analysis:');
|
||||
console.error('Error Type:', error.constructor.name);
|
||||
console.error('Error Message:', error.message);
|
||||
console.error('Error Stack:', error.stack);
|
||||
|
||||
// Additional debugging info
|
||||
console.error('\n🔍 Additional Debug Info:');
|
||||
console.error('Error Properties:', Object.getOwnPropertyNames(error));
|
||||
console.error('Error Code:', error.code);
|
||||
console.error('Error SQL:', error.sql);
|
||||
console.error('Error SQL Message:', error.sqlMessage);
|
||||
}
|
||||
}
|
||||
|
||||
runDetailedDebug();
|
||||
16
backend/debug-map.js
Normal file
16
backend/debug-map.js
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Debug script for map processing
|
||||
const debugInterface = require('./server.js');
|
||||
|
||||
console.log('🔧 Map Processor Debug Console');
|
||||
console.log('Available commands:');
|
||||
console.log(' assembleMap() - Assemble world map');
|
||||
console.log(' getTiles() - Get all map tiles');
|
||||
console.log(' getMetadata() - Get map metadata');
|
||||
console.log(' testCoords(x, z) - Test coordinate conversion');
|
||||
console.log('');
|
||||
|
||||
// Run assembleMap by default
|
||||
console.log('🚀 Running map assembly...');
|
||||
debugInterface.assembleMap();
|
||||
248
backend/map-processor.js
Normal file
248
backend/map-processor.js
Normal file
@@ -0,0 +1,248 @@
|
||||
const sharp = require('sharp');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { db } = require('./database');
|
||||
|
||||
class MapProcessor {
|
||||
constructor() {
|
||||
this.mapDir = path.join(__dirname, 'uploads', 'map');
|
||||
this.outputDir = path.join(__dirname, 'uploads', 'processed');
|
||||
this.worldMapPath = path.join(this.outputDir, 'world-map.png');
|
||||
this.tileSize = 1024; // Xaero's World Map tiles are 1024x1024
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await fs.mkdir(this.outputDir, { recursive: true });
|
||||
console.log('Map processor initialized');
|
||||
} catch (error) {
|
||||
console.error('Error initializing map processor:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getMapTiles() {
|
||||
try {
|
||||
const files = await fs.readdir(this.mapDir);
|
||||
const tileFiles = files.filter(file => file.endsWith('.png'));
|
||||
|
||||
// Parse tile coordinates from filenames like "0_3_x-3584_z1536.png"
|
||||
const tiles = tileFiles.map(file => {
|
||||
const match = file.match(/(\d+)_(\d+)_x(-?\d+)_z(-?\d+)\.png/);
|
||||
if (match) {
|
||||
return {
|
||||
filename: file,
|
||||
gridX: parseInt(match[1]),
|
||||
gridZ: parseInt(match[2]),
|
||||
x: parseInt(match[3]),
|
||||
z: parseInt(match[4])
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}).filter(tile => tile !== null);
|
||||
|
||||
return tiles.sort((a, b) => {
|
||||
// Sort by grid coordinates
|
||||
if (a.gridX !== b.gridX) return a.gridX - b.gridX;
|
||||
return a.gridZ - b.gridZ;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error reading map tiles:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async calculateMapDimensions(tiles) {
|
||||
if (tiles.length === 0) return { width: 0, height: 0, offsetX: 0, offsetZ: 0 };
|
||||
|
||||
// Find the center tile (3_1_x-512_z-512.png)
|
||||
//const centerTile = tiles.find(t => t.filename === '3_1_x-512_z-512.png');
|
||||
|
||||
let centerX, centerZ;
|
||||
|
||||
//if (centerTile) {
|
||||
// Use the specified tile as center point
|
||||
// centerX = centerTile.x + (this.tileSize / 2); // Center of the tile
|
||||
// centerZ = centerTile.z + (this.tileSize / 2); // Center of the tile
|
||||
// console.log(`Using tile ${centerTile.filename} as center point at (${centerX}, ${centerZ})`);
|
||||
//} else {
|
||||
// Fallback to original logic if center tile not found
|
||||
centerX = 0;
|
||||
centerZ = 0;
|
||||
console.log('Center tile 3_1_x-512_z-512.png not found, using origin (0,0) as center');
|
||||
//}
|
||||
|
||||
// Calculate bounds relative to the center
|
||||
const minX = Math.min(...tiles.map(t => t.x));
|
||||
const maxX = Math.max(...tiles.map(t => t.x + this.tileSize));
|
||||
const minZ = Math.min(...tiles.map(t => t.z));
|
||||
const maxZ = Math.max(...tiles.map(t => t.z + this.tileSize));
|
||||
|
||||
const width = maxX - minX;
|
||||
const height = maxZ - minZ;
|
||||
|
||||
// Calculate offsets to make the center tile the origin (0,0)
|
||||
// We need to shift everything so that the center point becomes (0,0)
|
||||
const offsetX = -centerX;
|
||||
const offsetZ = -centerZ;
|
||||
|
||||
return { width, height, offsetX, offsetZ };
|
||||
}
|
||||
|
||||
async assembleWorldMap() {
|
||||
console.log('Starting world map assembly...');
|
||||
|
||||
const tiles = await this.getMapTiles();
|
||||
if (tiles.length === 0) {
|
||||
console.log('No map tiles found');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`Found ${tiles.length} map tiles`);
|
||||
|
||||
const { width, height, offsetX, offsetZ } = await this.calculateMapDimensions(tiles);
|
||||
|
||||
console.log(`Map dimensions: ${width}x${height}px`);
|
||||
console.log(`Offset: X=${offsetX}, Z=${offsetZ}`);
|
||||
|
||||
// Create a blank canvas for the world map
|
||||
const worldMap = sharp({
|
||||
create: {
|
||||
width: width,
|
||||
height: height,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
}
|
||||
});
|
||||
|
||||
// Composite each tile onto the world map
|
||||
const overlays = await Promise.all(tiles.map(async (tile) => {
|
||||
const tilePath = path.join(this.mapDir, tile.filename);
|
||||
const x = tile.x + offsetX;
|
||||
const y = tile.z + offsetZ;
|
||||
|
||||
return {
|
||||
input: tilePath,
|
||||
left: x,
|
||||
top: y
|
||||
};
|
||||
}));
|
||||
|
||||
try {
|
||||
await worldMap
|
||||
.composite(overlays)
|
||||
.png()
|
||||
.toFile(this.worldMapPath);
|
||||
|
||||
// Update map metadata in database
|
||||
await this.updateMapMetadata({
|
||||
width: width,
|
||||
height: height,
|
||||
offsetX: offsetX,
|
||||
offsetZ: offsetZ,
|
||||
tileSize: this.tileSize,
|
||||
lastUpdated: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log(`World map assembled: ${this.worldMapPath}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error assembling world map:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async updateMapMetadata(metadata) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const keys = Object.keys(metadata);
|
||||
const values = Object.values(metadata);
|
||||
|
||||
const updatePromises = keys.map((key, index) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
`INSERT INTO map_metadata (\`key\`, value) VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE value = VALUES(value)`,
|
||||
[key, values[index]],
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(updatePromises)
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
async getMapMetadata() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all('SELECT `key`, value FROM map_metadata', [], (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
const metadata = {};
|
||||
rows.forEach(row => {
|
||||
metadata[row.key] = row.value;
|
||||
});
|
||||
resolve(metadata);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getWorldMapUrl() {
|
||||
const metadata = await this.getMapMetadata();
|
||||
if (metadata.width && metadata.height) {
|
||||
return '/api/map/world-map';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getCoordinateConversion(x, z) {
|
||||
const metadata = await this.getMapMetadata();
|
||||
if (!metadata.width || !metadata.height) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
const offsetX = parseInt(metadata.offsetX) || 0;
|
||||
const offsetZ = parseInt(metadata.offsetZ) || 0;
|
||||
const width = parseInt(metadata.width);
|
||||
const height = parseInt(metadata.height);
|
||||
|
||||
// Convert Minecraft coordinates to pixel coordinates
|
||||
const pixelX = x + offsetX;
|
||||
const pixelY = z + offsetZ;
|
||||
|
||||
// Convert to percentage for frontend (0-100%)
|
||||
const percentX = (pixelX / width) * 100;
|
||||
const percentY = (pixelY / height) * 100;
|
||||
|
||||
return {
|
||||
x: percentX,
|
||||
y: percentY,
|
||||
pixelX: pixelX,
|
||||
pixelY: pixelY
|
||||
};
|
||||
}
|
||||
|
||||
async getMarkersWithCoordinates() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all('SELECT * FROM map_markers WHERE is_public = 1', [], async (err, markers) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
const markersWithCoords = await Promise.all(markers.map(async (marker) => {
|
||||
const coords = await this.getCoordinateConversion(marker.x_coord, marker.z_coord);
|
||||
return {
|
||||
...marker,
|
||||
coordinates: coords
|
||||
};
|
||||
}));
|
||||
resolve(markersWithCoords);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MapProcessor;
|
||||
@@ -8,6 +8,7 @@ const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { db, init } = require('./database');
|
||||
const MapProcessor = require('./map-processor');
|
||||
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
@@ -1951,6 +1952,396 @@ app.post('/api/data', (req, res) => { const { eventType, player, uuid, timestamp
|
||||
|
||||
|
||||
|
||||
// Helper function to convert images to WebP
|
||||
function convertToWebP(inputPath, outputPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sharp = require('sharp');
|
||||
sharp(inputPath)
|
||||
.webp({ quality: 80 })
|
||||
.toFile(outputPath)
|
||||
.then(info => {
|
||||
resolve(info.size);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize Map Processor
|
||||
const mapProcessor = new MapProcessor();
|
||||
mapProcessor.init();
|
||||
|
||||
// Debug console interface for map processing
|
||||
// Available when running as standalone script or when required as module
|
||||
const debugInterface = {
|
||||
assembleMap: async () => {
|
||||
try {
|
||||
console.log('🔄 Starting map assembly...');
|
||||
const success = await mapProcessor.assembleWorldMap();
|
||||
if (success) {
|
||||
console.log('✅ Map assembly completed successfully');
|
||||
} else {
|
||||
console.log('❌ Map assembly failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error during map assembly:', error.message);
|
||||
}
|
||||
},
|
||||
|
||||
getTiles: async () => {
|
||||
try {
|
||||
const tiles = await mapProcessor.getMapTiles();
|
||||
console.log(`📋 Found ${tiles.length} map tiles:`);
|
||||
tiles.forEach((tile, index) => {
|
||||
if (index < 5) { // Show first 5 tiles
|
||||
console.log(` ${tile.filename}: X=${tile.x}, Z=${tile.z}`);
|
||||
}
|
||||
});
|
||||
if (tiles.length > 5) {
|
||||
console.log(` ... and ${tiles.length - 5} more tiles`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting tiles:', error.message);
|
||||
}
|
||||
},
|
||||
|
||||
getMetadata: async () => {
|
||||
try {
|
||||
const metadata = await mapProcessor.getMapMetadata();
|
||||
console.log('📊 Map Metadata:');
|
||||
console.log(` Width: ${metadata.width}px`);
|
||||
console.log(` Height: ${metadata.height}px`);
|
||||
console.log(` Offset X: ${metadata.offsetX}`);
|
||||
console.log(` Offset Z: ${metadata.offsetZ}`);
|
||||
console.log(` Tile Size: ${metadata.tileSize}px`);
|
||||
console.log(` Last Updated: ${metadata.lastUpdated || 'Never'}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting metadata:', error.message);
|
||||
}
|
||||
},
|
||||
|
||||
testCoords: async (x, z) => {
|
||||
try {
|
||||
const coords = await mapProcessor.getCoordinateConversion(x, z);
|
||||
console.log(`📍 Coordinate Conversion for (${x}, ${z}):`);
|
||||
console.log(` Pixel X: ${coords.pixelX}`);
|
||||
console.log(` Pixel Y: ${coords.pixelY}`);
|
||||
console.log(` Percentage X: ${coords.x.toFixed(2)}%`);
|
||||
console.log(` Percentage Y: ${coords.y.toFixed(2)}%`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error converting coordinates:', error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Make debug interface available globally
|
||||
global.mapProcessor = mapProcessor;
|
||||
global.assembleMap = debugInterface.assembleMap;
|
||||
global.getTiles = debugInterface.getTiles;
|
||||
global.getMetadata = debugInterface.getMetadata;
|
||||
global.testCoords = debugInterface.testCoords;
|
||||
|
||||
// Export debug interface for module usage
|
||||
module.exports = debugInterface;
|
||||
|
||||
// If running as standalone script, show console interface
|
||||
if (require.main === module) {
|
||||
console.log('🔧 Map Processor Debug Console');
|
||||
console.log('Available commands:');
|
||||
console.log(' assembleMap() - Assemble world map');
|
||||
console.log(' getTiles() - Get all map tiles');
|
||||
console.log(' getMetadata() - Get map metadata');
|
||||
console.log(' testCoords(x, z) - Test coordinate conversion');
|
||||
console.log('');
|
||||
console.log('💡 Usage examples:');
|
||||
console.log(' node -e "require(\'./server.js\').assembleMap()"');
|
||||
console.log(' node -e "require(\'./server.js\').getTiles()"');
|
||||
console.log(' node -e "require(\'./server.js\').testCoords(0, 0)"');
|
||||
console.log('');
|
||||
console.log('🚀 Starting debug session...');
|
||||
}
|
||||
|
||||
// === MAP API ===
|
||||
|
||||
// Get world map metadata
|
||||
app.get('/api/map/metadata', (req, res) => {
|
||||
mapProcessor.getMapMetadata()
|
||||
.then(metadata => {
|
||||
res.json({
|
||||
width: parseInt(metadata.width) || 0,
|
||||
height: parseInt(metadata.height) || 0,
|
||||
offsetX: parseInt(metadata.offsetX) || 0,
|
||||
offsetZ: parseInt(metadata.offsetZ) || 0,
|
||||
tileSize: parseInt(metadata.tileSize) || 1024,
|
||||
lastUpdated: metadata.lastUpdated || null
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error getting map metadata:', err);
|
||||
res.status(500).json({error: 'Fehler beim Laden der Karten-Metadaten'});
|
||||
});
|
||||
});
|
||||
|
||||
// Get assembled world map
|
||||
app.get('/api/map/world-map', (req, res) => {
|
||||
const worldMapPath = path.join(__dirname, 'uploads', 'processed', 'world-map.png');
|
||||
|
||||
if (fs.existsSync(worldMapPath)) {
|
||||
res.sendFile(worldMapPath);
|
||||
} else {
|
||||
res.status(404).json({error: 'Weltkarte nicht gefunden. Bitte zuerst zusammenstellen.'});
|
||||
}
|
||||
});
|
||||
|
||||
// Assemble world map from tiles
|
||||
app.post('/api/map/assemble', (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.status(401).send();
|
||||
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
|
||||
|
||||
console.log('🔄 Starting map assembly process...');
|
||||
|
||||
// Check if database is ready before assembling
|
||||
db.get("SELECT 1", [], (err) => {
|
||||
if (err) {
|
||||
console.error('❌ Database not ready for map assembly:', err);
|
||||
return res.status(500).json({
|
||||
error: 'Datenbank nicht bereit für Karten-Zusammenstellung',
|
||||
details: err.message,
|
||||
stack: err.stack
|
||||
});
|
||||
}
|
||||
console.log('✅ Database connection verified');
|
||||
|
||||
mapProcessor.assembleWorldMap()
|
||||
.then(success => {
|
||||
if (success) {
|
||||
console.log('✅ Map assembly completed successfully');
|
||||
res.json({success: true, message: 'Weltkarte erfolgreich zusammengestellt'});
|
||||
} else {
|
||||
console.error('❌ Map assembly failed - unknown error');
|
||||
res.status(500).json({
|
||||
error: 'Karten-Zusammenstellung fehlgeschlagen',
|
||||
details: 'Unbekannter Fehler bei der Karten-Zusammenstellung'
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('❌ Error assembling world map:', err);
|
||||
res.status(500).json({
|
||||
error: 'Fehler beim Zusammensetzen der Weltkarte',
|
||||
details: err.message,
|
||||
stack: err.stack,
|
||||
type: err.constructor.name
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Get all markers with coordinates
|
||||
app.get('/api/map/markers', (req, res) => {
|
||||
mapProcessor.getMarkersWithCoordinates()
|
||||
.then(markers => {
|
||||
res.json(markers);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error getting markers:', err);
|
||||
res.status(500).json({error: 'Fehler beim Laden der Marker'});
|
||||
});
|
||||
});
|
||||
|
||||
// Get public markers only
|
||||
app.get('/api/map/markers/public', (req, res) => {
|
||||
db.all('SELECT * FROM map_markers WHERE is_public = 1', [], async (err, markers) => {
|
||||
if (err) {
|
||||
console.error('Error getting public markers:', err);
|
||||
return res.status(500).json({error: 'Fehler beim Laden der öffentlichen Marker'});
|
||||
}
|
||||
|
||||
const markersWithCoords = await Promise.all(markers.map(async (marker) => {
|
||||
const coords = await mapProcessor.getCoordinateConversion(marker.x_coord, marker.z_coord);
|
||||
return {
|
||||
...marker,
|
||||
coordinates: coords
|
||||
};
|
||||
}));
|
||||
|
||||
res.json(markersWithCoords);
|
||||
});
|
||||
});
|
||||
|
||||
// Create new marker (Admin only)
|
||||
app.post('/api/map/markers', (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.status(401).send();
|
||||
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
|
||||
|
||||
const { name, type, x_coord, z_coord, description, linked_entity_type, linked_entity_id, icon_type, color } = req.body;
|
||||
|
||||
if (!name || x_coord === undefined || z_coord === undefined) {
|
||||
return res.status(400).json({error: 'Name und Koordinaten sind erforderlich'});
|
||||
}
|
||||
|
||||
// Generate unique ID
|
||||
const markerId = 'marker_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
const markerData = {
|
||||
id: markerId,
|
||||
name: name.trim(),
|
||||
type: type || 'poi',
|
||||
x_coord: parseInt(x_coord),
|
||||
z_coord: parseInt(z_coord),
|
||||
description: description || '',
|
||||
linked_entity_type: linked_entity_type || null,
|
||||
linked_entity_id: linked_entity_id || null,
|
||||
icon_type: icon_type || 'flag',
|
||||
color: color || '#2563eb',
|
||||
is_public: 1
|
||||
};
|
||||
|
||||
db.run(`INSERT INTO map_markers (id, name, type, x_coord, z_coord, description, linked_entity_type, linked_entity_id, icon_type, color, is_public)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[markerData.id, markerData.name, markerData.type, markerData.x_coord, markerData.z_coord,
|
||||
markerData.description, markerData.linked_entity_type, markerData.linked_entity_id,
|
||||
markerData.icon_type, markerData.color, markerData.is_public],
|
||||
function(err) {
|
||||
if (err) {
|
||||
console.error('Error creating marker:', err);
|
||||
return res.status(500).json({error: 'Fehler beim Erstellen des Markers'});
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
markerId: markerId,
|
||||
message: 'Marker erfolgreich erstellt'
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Update marker (Admin only)
|
||||
app.put('/api/map/markers/:markerId', (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.status(401).send();
|
||||
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
|
||||
|
||||
const { markerId } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
// Build dynamic update query
|
||||
const allowedFields = ['name', 'type', 'x_coord', 'z_coord', 'description', 'linked_entity_type', 'linked_entity_id', 'icon_type', 'color', 'is_public'];
|
||||
const updateFields = [];
|
||||
const values = [];
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (updates[field] !== undefined) {
|
||||
updateFields.push(`${field} = ?`);
|
||||
values.push(field.includes('_coord') ? parseInt(updates[field]) : updates[field]);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({error: 'Keine gültigen Felder zum Aktualisieren'});
|
||||
}
|
||||
|
||||
const query = `UPDATE map_markers SET ${updateFields.join(', ')} WHERE id = ?`;
|
||||
values.push(markerId);
|
||||
|
||||
db.run(query, values, function(err) {
|
||||
if (err) {
|
||||
console.error('Error updating marker:', err);
|
||||
return res.status(500).json({error: 'Fehler beim Aktualisieren des Markers'});
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({error: 'Marker nicht gefunden'});
|
||||
}
|
||||
res.json({success: true, message: 'Marker erfolgreich aktualisiert'});
|
||||
});
|
||||
});
|
||||
|
||||
// Delete marker (Admin only)
|
||||
app.delete('/api/map/markers/:markerId', (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.status(401).send();
|
||||
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
|
||||
|
||||
const { markerId } = req.params;
|
||||
|
||||
db.run("DELETE FROM map_markers WHERE id = ?", [markerId], function(err) {
|
||||
if (err) {
|
||||
console.error('Error deleting marker:', err);
|
||||
return res.status(500).json({error: 'Fehler beim Löschen des Markers'});
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({error: 'Marker nicht gefunden'});
|
||||
}
|
||||
res.json({success: true, message: 'Marker erfolgreich gelöscht'});
|
||||
});
|
||||
});
|
||||
|
||||
// Get map layers
|
||||
app.get('/api/map/layers', (req, res) => {
|
||||
db.all('SELECT * FROM map_layers ORDER BY order_index', [], (err, rows) => {
|
||||
if (err) {
|
||||
console.error('Error getting map layers:', err);
|
||||
return res.status(500).json({error: 'Fehler beim Laden der Karten-Layer'});
|
||||
}
|
||||
res.json(rows);
|
||||
});
|
||||
});
|
||||
|
||||
// Update map layer visibility/order (Admin only)
|
||||
app.put('/api/map/layers/:layerId', (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.status(401).send();
|
||||
if (!req.user.isAdmin) return res.status(403).json({error: 'Admin-Berechtigung erforderlich'});
|
||||
|
||||
const { layerId } = req.params;
|
||||
const { is_active, order_index } = req.body;
|
||||
|
||||
const updateFields = [];
|
||||
const values = [];
|
||||
|
||||
if (is_active !== undefined) {
|
||||
updateFields.push('is_active = ?');
|
||||
values.push(is_active ? 1 : 0);
|
||||
}
|
||||
|
||||
if (order_index !== undefined) {
|
||||
updateFields.push('order_index = ?');
|
||||
values.push(parseInt(order_index));
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({error: 'Keine gültigen Felder zum Aktualisieren'});
|
||||
}
|
||||
|
||||
const query = `UPDATE map_layers SET ${updateFields.join(', ')} WHERE id = ?`;
|
||||
values.push(layerId);
|
||||
|
||||
db.run(query, values, function(err) {
|
||||
if (err) {
|
||||
console.error('Error updating map layer:', err);
|
||||
return res.status(500).json({error: 'Fehler beim Aktualisieren des Layers'});
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({error: 'Layer nicht gefunden'});
|
||||
}
|
||||
res.json({success: true, message: 'Layer erfolgreich aktualisiert'});
|
||||
});
|
||||
});
|
||||
|
||||
// Coordinate conversion endpoint
|
||||
app.get('/api/map/convert-coords', (req, res) => {
|
||||
const { x, z } = req.query;
|
||||
|
||||
if (x === undefined || z === undefined) {
|
||||
return res.status(400).json({error: 'X und Z Koordinaten sind erforderlich'});
|
||||
}
|
||||
|
||||
mapProcessor.getCoordinateConversion(parseInt(x), parseInt(z))
|
||||
.then(coords => {
|
||||
res.json(coords);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error converting coordinates:', err);
|
||||
res.status(500).json({error: 'Fehler bei der Koordinaten-Konvertierung'});
|
||||
});
|
||||
});
|
||||
|
||||
// Serve uploaded files statically
|
||||
app.use('/uploads', express.static(UPLOAD_DIR));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user