import React, { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { MapMarker, MapLayer, MapMetadata } from '../types'; import { authService } from '../services/AuthService'; import { DiscordUser } from '../types'; const WorldMap: React.FC = () => { const navigate = useNavigate(); const [markers, setMarkers] = useState([]); const [layers, setLayers] = useState([]); const [mapMetadata, setMapMetadata] = useState(null); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Map interaction state const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const [mapPosition, setMapPosition] = useState({ x: 0, y: 0 }); const [scale, setScale] = useState(1); const [showAdminPanel, setShowAdminPanel] = useState(false); const [selectedMarker, setSelectedMarker] = useState(null); const [isAddingMarker, setIsAddingMarker] = useState(false); const [showOriginPin, setShowOriginPin] = useState(false); // Map assembly state const [isAssembling, setIsAssembling] = useState(false); const [assemblyProgress, setAssemblyProgress] = useState(''); const mapContainerRef = useRef(null); const mapImageRef = useRef(null); // Subscribe to auth changes useEffect(() => { const unsub = authService.subscribe((currentUser) => { setUser(currentUser); }); return unsub; }, []); // Load map data useEffect(() => { const loadData = async () => { try { setLoading(true); setError(null); // Load map metadata (may not exist if map hasn't been assembled) let metadata = null; try { const metadataResponse = await fetch('/api/map/metadata'); if (metadataResponse.ok) { metadata = await metadataResponse.json(); setMapMetadata(metadata); } else { console.log('Map metadata not found - map may not be assembled yet'); } } catch (metaErr) { console.log('Map metadata not available:', metaErr); } // Load layers const layersResponse = await fetch('/api/map/layers'); if (!layersResponse.ok) { console.error('Layers API error:', layersResponse.status, layersResponse.statusText); throw new Error(`Fehler beim Laden der Layer: ${layersResponse.status}`); } let layerData; try { layerData = await layersResponse.json(); } catch (jsonErr) { console.error('Layers JSON parse error:', jsonErr); throw new Error('Fehler beim Verarbeiten der Layer-Daten'); } setLayers(layerData); // Load markers const markersResponse = await fetch('/api/map/markers/public'); if (!markersResponse.ok) { console.error('Markers API error:', markersResponse.status, markersResponse.statusText); throw new Error(`Fehler beim Laden der Marker: ${markersResponse.status}`); } let markerData; try { markerData = await markersResponse.json(); } catch (jsonErr) { console.error('Markers JSON parse error:', jsonErr); throw new Error('Fehler beim Verarbeiten der Marker-Daten'); } setMarkers(markerData); } catch (err) { console.error('Error loading map data:', err); setError(err instanceof Error ? err.message : 'Unbekannter Fehler'); } finally { setLoading(false); } }; loadData(); }, []); // Map interaction handlers const handleMouseDown = (e: React.MouseEvent) => { if (!mapContainerRef.current) return; setIsDragging(true); setDragStart({ x: e.clientX - mapPosition.x, y: e.clientY - mapPosition.y }); }; const handleMouseMove = (e: MouseEvent) => { if (!isDragging || !mapContainerRef.current) return; const newX = e.clientX - dragStart.x; const newY = e.clientY - dragStart.y; setMapPosition({ x: newX, y: newY }); }; const handleMouseUp = () => { setIsDragging(false); }; const handleWheel = (e: WheelEvent) => { if (!mapContainerRef.current) return; e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; const newScale = Math.max(0.5, Math.min(3, scale * delta)); // Adjust position to zoom towards mouse cursor const mouseX = e.clientX; const mouseY = e.clientY; const containerRect = mapContainerRef.current.getBoundingClientRect(); const containerCenterX = containerRect.width / 2; const containerCenterY = containerRect.height / 2; const currentScale = scale; const newScaleValue = newScale; const offsetX = (mouseX - containerCenterX) * (1 - newScaleValue / currentScale); const offsetY = (mouseY - containerCenterY) * (1 - newScaleValue / currentScale); setMapPosition({ x: mapPosition.x - offsetX, y: mapPosition.y - offsetY }); setScale(newScale); }; useEffect(() => { if (isDragging) { document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseleave', handleMouseUp); } else { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseleave', handleMouseUp); } return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseleave', handleMouseUp); }; }, [isDragging, dragStart.x, dragStart.y, mapPosition.x, mapPosition.y]); useEffect(() => { const container = mapContainerRef.current; if (container) { container.addEventListener('wheel', handleWheel, { passive: false }); } return () => { if (container) { container.removeEventListener('wheel', handleWheel); } }; }, [scale, mapPosition.x, mapPosition.y]); // Handle marker clicks const handleMarkerClick = (marker: MapMarker) => { setSelectedMarker(marker); // Navigate to linked entity if exists if (marker.linked_entity_type && marker.linked_entity_id) { if (marker.linked_entity_type === 'organization' || marker.linked_entity_type === 'city') { navigate(`/cities/${marker.linked_entity_id}`); } } }; // Admin functions const handleAddMarker = (e: React.MouseEvent) => { if (!user?.isAdmin || !mapContainerRef.current || !mapImageRef.current) return; const rect = mapContainerRef.current.getBoundingClientRect(); const imageRect = mapImageRef.current.getBoundingClientRect(); // Calculate relative position within the image const relativeX = e.clientX - imageRect.left; const relativeY = e.clientY - imageRect.top; // Convert to percentage const percentX = (relativeX / imageRect.width) * 100; const percentY = (relativeY / imageRect.height) * 100; // Convert percentage to Minecraft coordinates using metadata if (mapMetadata) { const pixelX = (percentX / 100) * mapMetadata.width; const pixelY = (percentY / 100) * mapMetadata.height; // Convert back to Minecraft coordinates const minecraftX = pixelX - mapMetadata.offsetX; const minecraftZ = pixelY - mapMetadata.offsetZ; // Create new marker const newMarker = { name: 'Neuer Marker', type: 'poi' as const, x_coord: Math.round(minecraftX), z_coord: Math.round(minecraftZ), description: '', linked_entity_type: null, linked_entity_id: null, icon_type: 'flag', color: '#2563eb', is_public: 1 }; // Send to server fetch('/api/map/markers', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(newMarker) }) .then(response => response.json()) .then(data => { if (data.success) { // Reload markers return fetch('/api/map/markers/public'); } else { throw new Error(data.message || 'Fehler beim Erstellen des Markers'); } }) .then(response => response.json()) .then(markerData => { setMarkers(markerData); }) .catch(err => { console.error('Error creating marker:', err); setError(err.message); }); } }; const handleDeleteMarker = (markerId: string) => { if (!user?.isAdmin) return; fetch(`/api/map/markers/${markerId}`, { method: 'DELETE' }) .then(response => response.json()) .then(data => { if (data.success) { setMarkers(markers.filter(m => m.id !== markerId)); setSelectedMarker(null); } else { throw new Error(data.message || 'Fehler beim Löschen des Markers'); } }) .catch(err => { console.error('Error deleting marker:', err); setError(err.message); }); }; // Map assembly function with progress tracking const handleAssembleMap = async () => { if (!user?.isAdmin || isAssembling) return; setIsAssembling(true); setAssemblyProgress('Starte Karten-Zusammenstellung...'); try { setAssemblyProgress('Überprüfe Kacheln...'); // First check if tiles exist const tilesResponse = await fetch('/api/map/metadata'); if (!tilesResponse.ok) { setAssemblyProgress('Keine Kacheln gefunden. Bitte stellen Sie sicher, dass PNG-Dateien im Ordner backend/uploads/map/ vorhanden sind.'); setTimeout(() => { setIsAssembling(false); setAssemblyProgress(''); }, 3000); return; } setAssemblyProgress('Starte Karten-Zusammenstellung...'); const response = await fetch('/api/map/assemble', { method: 'POST', headers: { 'Content-Type': 'application/json', } }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.error || `Server-Fehler: ${response.status}`; const errorDetails = errorData.details || ''; const errorStack = errorData.stack || ''; console.error('Map assembly error details:', { status: response.status, error: errorMessage, details: errorDetails, stack: errorStack }); setAssemblyProgress(`Fehler: ${errorMessage}`); // Show detailed error in alert for debugging if (errorDetails) { alert(`Karten-Zusammenstellung fehlgeschlagen:\n\nFehler: ${errorMessage}\n\nDetails: ${errorDetails}\n\nStack: ${errorStack}`); } else { alert(`Karten-Zusammenstellung fehlgeschlagen:\n\nFehler: ${errorMessage}`); } setTimeout(() => { setIsAssembling(false); setAssemblyProgress(''); }, 5000); return; } const result = await response.json(); if (result.success) { setAssemblyProgress('Karte erfolgreich zusammengestellt!'); // Reload map metadata and markers setTimeout(async () => { try { const metadataResponse = await fetch('/api/map/metadata'); if (metadataResponse.ok) { const metadata = await metadataResponse.json(); setMapMetadata(metadata); } const markersResponse = await fetch('/api/map/markers/public'); if (markersResponse.ok) { const markerData = await markersResponse.json(); setMarkers(markerData); } setIsAssembling(false); setAssemblyProgress(''); alert('Weltkarte wurde erfolgreich zusammengestellt!'); } catch (err) { console.error('Error reloading map data:', err); setIsAssembling(false); setAssemblyProgress(''); } }, 1000); } else { throw new Error(result.message || 'Fehler beim Zusammensetzen der Karte'); } } catch (err) { console.error('Error assembling map:', err); setAssemblyProgress(`Fehler: ${err.message}`); setTimeout(() => { setIsAssembling(false); setAssemblyProgress(''); }, 3000); } }; if (loading) { return (
Lade Weltkarte...
); } if (error) { return (
Fehler: {error}
); } return (
{/* Sidebar */}

Weltkarte

{user?.isAdmin && ( )}
{/* Map Controls */}
Zoom
Mausrad zum Zoomen, Klicken zum Verschieben
{/* Origin Controls */}
Origin (0,0)
Positioniert den Ursprung (0,0) in der Mitte der Karte
{/* Layers */}

Layer

{layers.map(layer => ( ))}
{/* Markers List */}

Orte

{markers.length === 0 ? (

Keine Marker gefunden

) : ( markers.map(marker => (
handleMarkerClick(marker)} className={`p-2 rounded mb-2 cursor-pointer hover:bg-bgPrimary ${ selectedMarker?.id === marker.id ? 'bg-bgPrimary' : '' }`} >
{marker.name}
{marker.type} • {marker.x_coord}, {marker.z_coord}
{marker.description && (

{marker.description}

)}
)) )}
{/* Map Container */}
{mapMetadata ? ( Weltkarte { // Center the map and position origin (0,0) in the center if (mapImageRef.current && mapContainerRef.current && mapMetadata) { const img = mapImageRef.current; const container = mapContainerRef.current; const containerRect = container.getBoundingClientRect(); // Calculate where the origin (0,0) is in the map image pixels const originXInPixels = mapMetadata.offsetX; const originYInPixels = mapMetadata.offsetZ; // Calculate the center of the viewport const viewportCenterX = containerRect.width / 2; const viewportCenterY = containerRect.height / 2; // Position the map so that origin (0,0) is in the center of the viewport // We need to position the image so that the origin pixel is at the viewport center const mapX = viewportCenterX - originXInPixels; const mapY = viewportCenterY - originYInPixels; setMapPosition({ x: mapX, y: mapY }); } }} /> ) : (
🗺️

Weltkarte nicht verfügbar

Die Weltkarte muss zuerst im Admin-Bereich zusammengestellt werden.

{user?.isAdmin ? (

1. Klicken Sie auf "Admin" oben rechts

2. Klicken Sie auf "Karte neu zusammensetzen"

3. Warten Sie, bis die Verarbeitung abgeschlossen ist

) : (

Bitte wenden Sie sich an einen Administrator, um die Weltkarte zu erstellen.

)}
)} {/* Markers Overlay */} {markers.map(marker => (
handleMarkerClick(marker)} title={marker.name} >
{marker.name}
))} {/* Admin Panel */} {showAdminPanel && user?.isAdmin && (

Admin-Tools

{/* Map Assembly Section */}

Karten-Zusammenstellung

{isAssembling ? (
{assemblyProgress}
Bitte warten Sie, bis der Vorgang abgeschlossen ist...
) : (
{mapMetadata ? 'Aktualisiert die bestehende Weltkarte mit neuen Kacheln' : 'Erstellt die Weltkarte aus den PNG-Kacheln im Upload-Ordner' }
)}
{/* Marker Management Section */}
Klicken Sie auf die Karte, um neue Marker hinzuzufügen
)} {/* Selected Marker Info */} {selectedMarker && (

{selectedMarker.name}

{selectedMarker.type}
Koordinaten: {selectedMarker.x_coord}, {selectedMarker.z_coord}
{selectedMarker.description && (

{selectedMarker.description}

)} {user?.isAdmin && (
)}
)}
); }; export default WorldMap;