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;