/** * Mindmap-Visualisierungsmodul mit Cytoscape.js * Erstellt eine interaktive Mindmap-Visualisierung */ // Globales MindMap-Objekt erstellen, falls es noch nicht existiert if (!window.MindMap) { window.MindMap = {}; } // MindMap Klasse hinzufügen window.MindMap.Visualization = class MindMapVisualization { /** * Konstruktor für die MindMap-Visualisierung * @param {string} containerId - ID des HTML-Containers für die Visualisierung * @param {object} options - Optionen für die Visualisierung */ constructor(containerId, options = {}) { this.containerId = containerId; this.container = document.getElementById(containerId.replace('#', '')); // Optionen mit Standardwerten this.options = { layout: options.layout || 'cose', darkMode: options.darkMode || document.documentElement.classList.contains('dark'), nodeClickCallback: options.nodeClickCallback || null, height: options.height || 600, ...options }; // Cytoscape-Instanz this.cy = null; // Vorbereitende Prüfungen if (!this.container) { console.error(`Container mit ID ${containerId} nicht gefunden`); return; } // Cytoscape-Bibliothek prüfen if (typeof cytoscape === 'undefined') { this._loadCytoscapeLibrary() .then(() => this.initialize()) .catch(error => { console.error('Cytoscape.js konnte nicht geladen werden:', error); this._showError('Cytoscape.js konnte nicht geladen werden. Bitte laden Sie die Seite neu.'); }); } else { this.initialize(); } } /** * Lädt die Cytoscape-Bibliothek dynamisch * @returns {Promise} Promise, das nach dem Laden erfüllt wird */ _loadCytoscapeLibrary() { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js'; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } /** * Initialisiert die Cytoscape-Instanz */ initialize() { // Container-Dimensionen festlegen this.container.style.height = `${this.options.height}px`; // Cytoscape-Instanz erstellen this.cy = cytoscape({ container: this.container, elements: [], style: this._createStylesheet(), layout: this._getLayoutOptions(), wheelSensitivity: 0.2, minZoom: 0.1, maxZoom: 3, boxSelectionEnabled: false }); // Event-Listener für Knoten-Klicks this.cy.on('tap', 'node', event => { const node = event.target; // Alle Knoten zurücksetzen this.cy.nodes().removeClass('selected'); // Ausgewählten Knoten markieren node.addClass('selected'); // Callback aufrufen, falls definiert if (this.options.nodeClickCallback) { this.options.nodeClickCallback(node.data()); } }); // Event-Listener für Hintergrund-Klicks (Selektion aufheben) this.cy.on('tap', event => { if (event.target === this.cy) { this.cy.nodes().removeClass('selected'); } }); // Dark Mode Änderungen überwachen document.addEventListener('darkModeToggled', event => { this.updateDarkMode(event.detail.isDark); }); } /** * Erstellt das Stylesheet für Cytoscape * @returns {Array} Array mit Stilregeln */ _createStylesheet() { const isDarkMode = this.options.darkMode; return [ { selector: 'node', style: { 'background-color': 'data(color)', 'label': 'data(name)', 'width': 30, 'height': 30, 'font-size': 12, 'text-valign': 'bottom', 'text-halign': 'center', 'text-margin-y': 8, 'color': isDarkMode ? '#f1f5f9' : '#334155', 'text-background-color': isDarkMode ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)', 'text-background-opacity': 0.8, 'text-background-padding': '2px', 'text-background-shape': 'roundrectangle', 'text-wrap': 'ellipsis', 'text-max-width': '100px' } }, { selector: 'edge', style: { 'width': 2, 'line-color': isDarkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)', 'target-arrow-color': isDarkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)', 'curve-style': 'bezier' } }, { selector: 'node.selected', style: { 'background-color': 'data(color)', 'border-width': 3, 'border-color': '#8b5cf6', 'width': 40, 'height': 40, 'font-size': 14, 'font-weight': 'bold', 'text-background-color': '#8b5cf6', 'text-background-opacity': 0.9 } } ]; } /** * Ermittelt die Layout-Optionen basierend auf dem gewählten Layout * @returns {object} Layout-Optionen für Cytoscape */ _getLayoutOptions() { const layouts = { 'cose': { name: 'cose', animate: true, animationDuration: 800, nodeDimensionsIncludeLabels: true, refresh: 30, randomize: true, componentSpacing: 100, nodeRepulsion: 8000, nodeOverlap: 20, idealEdgeLength: 200, edgeElasticity: 100, nestingFactor: 1.2, gravity: 80, fit: true, padding: 30 }, 'circle': { name: 'circle', fit: true, padding: 50, radius: 200, startAngle: 3 / 2 * Math.PI, sweep: 2 * Math.PI, clockwise: true, sort: (a, b) => a.data('name').localeCompare(b.data('name')) }, 'concentric': { name: 'concentric', fit: true, padding: 50, concentric: node => node.data('connections') || 1, levelWidth: () => 1, minNodeSpacing: 50 } }; return layouts[this.options.layout] || layouts['cose']; } /** * Zeigt eine Fehlermeldung im Container an * @param {string} message - Die anzuzeigende Fehlermeldung */ _showError(message) { this.container.innerHTML = `

${message}

Bitte laden Sie die Seite neu oder kontaktieren Sie den Support.

`; } /** * Zeigt eine Ladeanimation im Container an */ showLoading() { this.container.innerHTML = `
`; } /** * Lädt Daten aus der API und rendert sie */ loadData() { // Ladeanimation anzeigen this.showLoading(); // Versuche, Daten vom Server zu laden fetch('/api/mindmap') .then(response => { if (!response.ok) { throw new Error('Netzwerkfehler beim Laden der Mindmap-Daten'); } return response.json(); }) .then(data => { this.renderData(data); }) .catch(error => { console.error('Fehler beim Laden der Mindmap-Daten:', error); // Verwende Standarddaten als Fallback console.log('Verwende Standarddaten als Fallback...'); const defaultData = this._generateDefaultData(); this.renderData(defaultData); }); } /** * Generiert Standarddaten für die Mindmap als Fallback * @returns {object} Standarddaten für die Mindmap */ _generateDefaultData() { return { nodes: [ { id: 'root', name: 'Wissen', description: 'Zentrale Wissensbasis', category: 'Zentral', color_code: '#4299E1' }, { id: 'philosophy', name: 'Philosophie', description: 'Philosophisches Denken', category: 'Philosophie', color_code: '#9F7AEA', parent_id: 'root' }, { id: 'science', name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', category: 'Wissenschaft', color_code: '#48BB78', parent_id: 'root' }, { id: 'technology', name: 'Technologie', description: 'Technologische Entwicklungen', category: 'Technologie', color_code: '#ED8936', parent_id: 'root' }, { id: 'arts', name: 'Künste', description: 'Künstlerische Ausdrucksformen', category: 'Künste', color_code: '#ED64A6', parent_id: 'root' }, { id: 'psychology', name: 'Psychologie', description: 'Menschliches Verhalten und Geist', category: 'Psychologie', color_code: '#4299E1', parent_id: 'root' } ] }; } /** * Rendert die Daten in der Cytoscape-Instanz * @param {object} data - Daten für die Mindmap */ renderData(data) { if (!this.cy) { console.error('Cytoscape-Instanz nicht initialisiert'); return; } // Konvertiere die Daten in das Cytoscape-Format const elements = this._convertToCytoscapeFormat(data); // Aktualisiere die Elemente this.cy.elements().remove(); this.cy.add(elements); // Layout neu berechnen und anwenden const layout = this.cy.layout(this._getLayoutOptions()); layout.run(); // Zentriere und passe die Ansicht an setTimeout(() => { this.fitView(); }, 100); } /** * Konvertiert die Daten in das Cytoscape-Format * @param {object} data - Daten für die Mindmap * @returns {Array} Cytoscape-Elemente */ _convertToCytoscapeFormat(data) { const elements = []; // Knoten hinzufügen if (data.nodes && data.nodes.length > 0) { data.nodes.forEach(node => { elements.push({ group: 'nodes', data: { id: String(node.id), name: node.name, description: node.description || 'Keine Beschreibung verfügbar', category: node.category || 'Allgemein', color: node.color_code || this._getRandomColor(), connections: 0 } }); // Kante zum Elternknoten hinzufügen (falls vorhanden) if (node.parent_id) { elements.push({ group: 'edges', data: { id: `edge-${node.parent_id}-${node.id}`, source: String(node.parent_id), target: String(node.id) } }); } }); // Zusätzliche Kanten zwischen Knoten hinzufügen (falls in den Daten vorhanden) if (data.edges && data.edges.length > 0) { data.edges.forEach(edge => { elements.push({ group: 'edges', data: { id: `edge-${edge.source}-${edge.target}`, source: String(edge.source), target: String(edge.target) } }); }); } } return elements; } /** * Generiert eine zufällige Farbe * @returns {string} Zufällige Farbe als HEX-Code */ _getRandomColor() { const colors = [ '#4299E1', // Blau '#9F7AEA', // Lila '#48BB78', // Grün '#ED8936', // Orange '#ED64A6', // Pink '#F56565' // Rot ]; return colors[Math.floor(Math.random() * colors.length)]; } /** * Aktualisiert den Dark Mode für die Visualisierung * @param {boolean} isDark - Ob der Dark Mode aktiviert ist */ updateDarkMode(isDark) { if (!this.cy) return; this.options.darkMode = isDark; // Farben aktualisieren const textColor = isDark ? '#f1f5f9' : '#334155'; const textBgColor = isDark ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)'; const edgeColor = isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)'; // Stile aktualisieren this.cy.style() .selector('node') .style({ 'color': textColor, 'text-background-color': textBgColor }) .selector('edge') .style({ 'line-color': edgeColor, 'target-arrow-color': edgeColor }) .update(); } /** * Passt die Ansicht an die Mindmap an */ fitView() { if (!this.cy) return; this.cy.fit(); this.cy.center(); } /** * Zoomt in die Mindmap hinein */ zoomIn() { if (!this.cy) return; const zoom = this.cy.zoom() * 1.2; this.cy.zoom({ level: zoom, renderedPosition: { x: this.cy.width() / 2, y: this.cy.height() / 2 } }); } /** * Zoomt aus der Mindmap heraus */ zoomOut() { if (!this.cy) return; const zoom = this.cy.zoom() / 1.2; this.cy.zoom({ level: zoom, renderedPosition: { x: this.cy.width() / 2, y: this.cy.height() / 2 } }); } /** * Setzt den Zoom zurück */ resetZoom() { if (!this.cy) return; this.cy.zoom(1); this.cy.center(); } /** * Berechnet das Layout neu */ relayout() { if (!this.cy) return; const layout = this.cy.layout(this._getLayoutOptions()); layout.run(); } /** * Ändert das Layout * @param {string} layoutName - Name des Layouts */ changeLayout(layoutName) { if (!this.cy) return; this.options.layout = layoutName; const layout = this.cy.layout(this._getLayoutOptions()); layout.run(); } /** * Fokussiert einen Knoten * @param {string} nodeId - ID des zu fokussierenden Knotens */ focusNode(nodeId) { if (!this.cy) return; const node = this.cy.getElementById(nodeId); if (node.length > 0) { this.cy.nodes().removeClass('selected'); node.addClass('selected'); this.cy.center(node); // Callback aufrufen, falls definiert if (this.options.nodeClickCallback) { this.options.nodeClickCallback(node.data()); } } } /** * Sucht nach Knoten basierend auf einem Suchbegriff * @param {string} searchTerm - Suchbegriff */ search(searchTerm) { if (!this.cy || !searchTerm) return; const term = searchTerm.toLowerCase(); let found = false; // Alle Knoten durchsuchen this.cy.nodes().forEach(node => { const name = node.data('name').toLowerCase(); const category = node.data('category').toLowerCase(); const description = node.data('description').toLowerCase(); // Prüfen, ob der Suchbegriff enthalten ist if (name.includes(term) || category.includes(term) || description.includes(term)) { found = true; this.cy.nodes().removeClass('selected'); node.addClass('selected'); this.cy.center(node); // Callback nur für den ersten gefundenen Knoten aufrufen if (found && this.options.nodeClickCallback) { this.options.nodeClickCallback(node.data()); } return false; // Schleife abbrechen nach dem ersten Fund } }); } /** * Gibt die Daten des ausgewählten Knotens zurück * @returns {object|null} Daten des ausgewählten Knotens oder null */ getSelectedNodeData() { if (!this.cy) return null; const selectedNodes = this.cy.nodes('.selected'); if (selectedNodes.length > 0) { return selectedNodes[0].data(); } return null; } };