Files
project_vollidioten_website/backend/map-processor.js
Lars Behrends 065a6e657d 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
2026-01-02 05:08:07 +01:00

249 lines
8.4 KiB
JavaScript

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;