/** * Mindmap.js - Modul für die Mindmap-Visualisierung mit Cytoscape.js * Version: 1.0.0 * Datum: 01.05.2025 */ // Stellen Sie sicher, dass das globale MindMap-Objekt existiert if (!window.MindMap) { window.MindMap = {}; } /** * Mindmap-Visualisierungsklasse */ class MindmapVisualization { /** * Konstruktor für die Mindmap-Visualisierung * @param {string} containerId - ID des Container-Elements * @param {Object} options - Konfigurationsoptionen */ constructor(containerId, options = {}) { this.containerId = containerId; this.container = document.getElementById(containerId); this.cy = null; this.data = null; this.options = Object.assign({ defaultLayout: 'cose', darkMode: document.documentElement.classList.contains('dark'), apiEndpoint: '/api/mindmap', onNodeClick: null }, options); // Event-Listener für Dark Mode-Änderungen document.addEventListener('darkModeToggled', (event) => { this.options.darkMode = event.detail.isDark; if (this.cy) { this.updateStyles(); } }); } /** * Initialisiert die Mindmap */ async initialize() { console.log("Initialisiere Mindmap..."); try { // Versuche, Daten vom Server zu laden this.data = await this.loadDataFromServer(); console.log("Daten vom Server geladen:", this.data); } catch (error) { console.warn("Fehler beim Laden der Daten vom Server:", error); console.log("Verwende Standarddaten als Fallback"); this.data = this.getDefaultData(); } // Cytoscape initialisieren this.initializeCytoscape(); return this; } /** * Lädt Daten vom Server * @returns {Promise} - Promise mit den geladenen Daten */ async loadDataFromServer() { try { // Zuerst versuchen wir, Daten von /api/mindmap zu laden const response = await fetch(this.options.apiEndpoint); if (!response.ok) { throw new Error(`HTTP Fehler: ${response.status}`); } const data = await response.json(); // Wenn keine Daten vorhanden sind, versuche /api/refresh-mindmap if (!data.nodes || data.nodes.length === 0) { console.log("Keine Daten gefunden, versuche /api/refresh-mindmap"); const refreshResponse = await fetch('/api/refresh-mindmap'); if (!refreshResponse.ok) { throw new Error(`HTTP Fehler beim Refresh: ${refreshResponse.status}`); } return await refreshResponse.json(); } return data; } catch (error) { console.error("Fehler beim Laden der Daten:", error); throw error; } } /** * Liefert Standarddaten als Fallback * @returns {Object} - Standarddaten für die Mindmap */ getDefaultData() { return { nodes: [ { id: "root", name: "Wissen", description: "Zentrale Wissensbasis", color_code: "#4299E1" }, { id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", color_code: "#9F7AEA" }, { id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", color_code: "#48BB78" }, { id: "technology", name: "Technologie", description: "Technologische Entwicklungen", color_code: "#ED8936" }, { id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", color_code: "#ED64A6" } ], edges: [ { source: "root", target: "philosophy" }, { source: "root", target: "science" }, { source: "root", target: "technology" }, { source: "root", target: "arts" } ] }; } /** * Initialisiert Cytoscape.js */ initializeCytoscape() { if (!this.container) { console.error(`Container mit ID '${this.containerId}' nicht gefunden.`); return; } // Konvertiert die API-Daten ins Cytoscape-Format const elements = this.convertToCytoscapeFormat(this.data); // Erstellt die Cytoscape-Instanz this.cy = cytoscape({ container: this.container, elements: elements, style: this.getStyles(), layout: { name: this.options.defaultLayout, animate: true, animationDuration: 800, nodeDimensionsIncludeLabels: true, padding: 30, spacingFactor: 1.2, randomize: true, componentSpacing: 100, nodeRepulsion: 8000, edgeElasticity: 100, nestingFactor: 1.2, gravity: 80 } }); // Event-Listener für Knotenklicks this.cy.on('tap', 'node', (event) => { const node = event.target; this.handleNodeClick(node); }); console.log("Cytoscape initialisiert"); } /** * Konvertiert API-Daten ins Cytoscape-Format * @param {Object} data - Daten von der API * @returns {Array} - Elemente im Cytoscape-Format */ convertToCytoscapeFormat(data) { const elements = []; // Überprüfen, ob wir Daten aus der API haben if (data.nodes && Array.isArray(data.nodes)) { // Füge zuerst einen Root-Knoten "Wissen" hinzu, falls er nicht existiert let rootExists = data.nodes.some(node => node.name === "Wissen"); if (!rootExists) { elements.push({ data: { id: "root", name: "Wissen", description: "Zentrale Wissensbasis", color: "#4299E1", isRoot: true } }); } // Knoten hinzufügen data.nodes.forEach(node => { // Prüfen, ob es sich um einen Knoten-Objekt oder einen Baum handelt const nodeId = node.id || `node-${elements.length}`; const nodeName = node.name; const nodeDesc = node.description || ""; const nodeColor = node.color_code || '#8B5CF6'; elements.push({ data: { id: nodeId, name: nodeName, description: nodeDesc, color: nodeColor, category: node.category_id, isRoot: node.name === "Wissen" } }); // Falls es ein hierarchischer Baum ist mit Kindern if (node.children && Array.isArray(node.children)) { this.processChildNodes(node, elements, nodeId); } }); // Kanten hinzufügen - bei hierarchischen Daten if (!data.edges || !Array.isArray(data.edges)) { // Wenn keine Kanten definiert sind, verknüpfe alle Root-Knoten mit dem "Wissen" Knoten const rootId = elements.find(el => el.data.isRoot)?.data.id || "root"; elements.forEach(element => { if (element.data.id !== rootId && !element.data.hasParent) { elements.push({ data: { id: `${rootId}-${element.data.id}`, source: rootId, target: element.data.id } }); } }); } else { // Wenn Kanten definiert sind, diese direkt verwenden data.edges.forEach(edge => { elements.push({ data: { id: `${edge.source}-${edge.target}`, source: edge.source, target: edge.target } }); }); } } return elements; } /** * Verarbeitet Kindknoten rekursiv für hierarchische Daten * @param {Object} node - Der aktuelle Knoten * @param {Array} elements - Das Element-Array * @param {string} parentId - Die ID des Elternknotens */ processChildNodes(node, elements, parentId) { if (node.children && Array.isArray(node.children)) { node.children.forEach(child => { const childId = child.id || `node-${elements.length}`; const childName = child.name; const childDesc = child.description || ""; const childColor = child.color_code || '#8B5CF6'; elements.push({ data: { id: childId, name: childName, description: childDesc, color: childColor, category: child.category_id, hasParent: true } }); // Kante zum Elternknoten elements.push({ data: { id: `${parentId}-${childId}`, source: parentId, target: childId } }); // Rekursiv Kinder verarbeiten this.processChildNodes(child, elements, childId); }); } } /** * Liefert die Styles für Cytoscape * @returns {Array} - Cytoscape-Stylesheets */ getStyles() { const darkMode = this.options.darkMode; return [ { selector: 'node', style: { 'background-color': 'data(color)', 'label': 'data(name)', 'width': 40, 'height': 40, 'font-size': 12, 'text-valign': 'bottom', 'text-halign': 'center', 'text-margin-y': 8, 'color': darkMode ? '#f1f5f9' : '#334155', 'text-background-color': darkMode ? '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: 'node[?isRoot]', style: { 'width': 60, 'height': 60, 'font-size': 14, 'font-weight': 'bold', 'text-background-opacity': 0.9, 'text-background-color': '#4299E1' } }, { selector: 'edge', style: { 'width': 2, 'line-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)', 'target-arrow-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)', 'curve-style': 'bezier', 'target-arrow-shape': 'triangle' } }, { selector: 'node:selected', style: { 'background-color': 'data(color)', 'border-width': 3, 'border-color': '#8b5cf6', 'width': 50, 'height': 50, 'font-size': 14, 'font-weight': 'bold', 'text-background-color': '#8b5cf6', 'text-background-opacity': 0.9 } } ]; } /** * Aktualisiert die Styles basierend auf dem Dark Mode */ updateStyles() { this.cy.style(this.getStyles()); } /** * Behandelt den Klick auf einen Knoten * @param {Object} node - Cytoscape-Knoten */ handleNodeClick(node) { console.log("Knoten angeklickt:", node.id(), node.data()); // Callback aufrufen, falls definiert if (typeof this.options.onNodeClick === 'function') { this.options.onNodeClick(node.data()); } } /** * Passt die Ansicht an alle Elemente an */ fitToElements() { if (this.cy) { this.cy.fit(); this.cy.center(); } } /** * Setzt das Layout zurück * @param {string} layoutName - Name des Layouts (optional) */ resetLayout(layoutName) { if (this.cy) { this.cy.layout({ name: layoutName || this.options.defaultLayout, animate: true, randomize: true, fit: true }).run(); } } } // Globale Export window.MindMap.Visualization = MindmapVisualization; // Automatische Initialisierung, wenn das DOM geladen ist document.addEventListener('DOMContentLoaded', function() { const cyContainer = document.getElementById('cy'); if (cyContainer) { console.log("Mindmap-Container gefunden, initialisiere..."); const mindmap = new MindmapVisualization('cy', { onNodeClick: function(nodeData) { console.log("Knoten ausgewählt:", nodeData); // Hier könnte man weitere Aktionen durchführen } }); mindmap.initialize().then(() => { console.log("Mindmap erfolgreich initialisiert"); // Speichere die Instanz global für den Zugriff von außen window.mindmap = mindmap; }).catch(error => { console.error("Fehler bei der Initialisierung der Mindmap:", error); }); } });