419 lines
29 KiB
JavaScript
419 lines
29 KiB
JavaScript
/**
|
||
* 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);
|
||
});
|
||
}
|
||
});
|