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:
Lars Behrends
2026-01-02 05:08:07 +01:00
parent ea2b803534
commit 065a6e657d
152 changed files with 5024 additions and 35 deletions

View File

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