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:
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;
|
||||
Reference in New Issue
Block a user