/**
* BaseVisualizer - Abstrakte Basis-Klasse für alle Visualizer
* Definiert gemeinsames Interface für postMessage-Kommunikation, Rendering und State-Management
*
* Alle Visualizer (TreeViz, Flowchart, RuleViz, NeuralNet, RL, MonteCarlo) erben von dieser Klasse.
*
* @author Alexander Wolf
* @version 1.0
*/
class BaseVisualizer {
/**
* Erstellt einen neuen Visualizer
* @param {HTMLCanvasElement} canvas - Canvas-Element für Rendering
* @param {Object} options - Konfigurationsoptions
*/
constructor(canvas, options = {}) {
if (new.target === BaseVisualizer) {
throw new TypeError('Cannot instantiate abstract class BaseVisualizer');
}
/**
* Das Canvas-Element
* @type {HTMLCanvasElement}
*/
this.canvas = canvas;
/**
* 2D Rendering Context
* @type {CanvasRenderingContext2D}
*/
this.ctx = canvas.getContext('2d');
/**
* Konfiguration
* @type {Object}
*/
this.config = {
showOverlay: options.showOverlay !== undefined ? options.showOverlay : true,
enableInteraction: options.enableInteraction !== undefined ? options.enableInteraction : true,
...options
};
/**
* Viewport-State (für Zoom/Pan)
* @type {Object}
*/
this.viewport = {
scale: 1.0,
offsetX: 0,
offsetY: 0,
minScale: 0.1,
maxScale: 3.0
};
/**
* Interaction-State
* @type {Object}
*/
this.interaction = {
isDragging: false,
dragStart: { x: 0, y: 0 },
lastMousePos: { x: 0, y: 0 }
};
/**
* postMessage Communication State
* @type {boolean}
*/
this.ready = false;
// Setup & Initialize
this._setupCanvas();
this._setupPostMessage();
if (this.config.enableInteraction) {
this._setupInteraction();
}
}
/**
* Konfiguriert das Canvas (Größe, Resize-Handler)
* @private
*/
_setupCanvas() {
const container = this.canvas.parentElement;
this.canvas.width = container.clientWidth;
this.canvas.height = container.clientHeight;
// Resize handler
window.addEventListener('resize', () => {
this.canvas.width = container.clientWidth;
this.canvas.height = container.clientHeight;
if (this.nodes || this.data) {
this.render();
}
});
}
/**
* Konfiguriert postMessage Listener für externe Commands
* Subklassen können diese erweitern
* @private
*/
_setupPostMessage() {
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'VIZ_COMMAND') {
this.executeCommand(event.data.command);
}
});
// Signal readiness to parent
DebugConfig.log(DEBUG_DOMAINS.VIZ_BASE, "debug", `${this.constructor.name}: Sending READY signal`);
window.parent.postMessage({ type: 'VIZ_READY', visualizer: this.constructor.name }, '*');
this.ready = true;
}
/**
* Konfiguriert Zoom/Pan/Touch Interaktion
* Implementiert Standard-Zoom und Pan für alle Visualizer
* @private
*/
_setupInteraction() {
// Mouse wheel zoom
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
const newScale = Math.max(
this.viewport.minScale,
Math.min(this.viewport.maxScale, this.viewport.scale * zoomFactor)
);
const scaleRatio = newScale / this.viewport.scale;
this.viewport.offsetX = mouseX - (mouseX - this.viewport.offsetX) * scaleRatio;
this.viewport.offsetY = mouseY - (mouseY - this.viewport.offsetY) * scaleRatio;
this.viewport.scale = newScale;
this.render();
});
// Mouse drag pan
this.canvas.addEventListener('mousedown', (e) => {
this.interaction.isDragging = true;
const rect = this.canvas.getBoundingClientRect();
this.interaction.dragStart.x = e.clientX - rect.left - this.viewport.offsetX;
this.interaction.dragStart.y = e.clientY - rect.top - this.viewport.offsetY;
this.canvas.style.cursor = 'grabbing';
});
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
if (this.interaction.isDragging) {
this.viewport.offsetX = mouseX - this.interaction.dragStart.x;
this.viewport.offsetY = mouseY - this.interaction.dragStart.y;
this.render();
}
this.interaction.lastMousePos = { x: mouseX, y: mouseY };
});
this.canvas.addEventListener('mouseup', () => {
this.interaction.isDragging = false;
this.canvas.style.cursor = 'grab';
});
this.canvas.addEventListener('mouseleave', () => {
this.interaction.isDragging = false;
this.canvas.style.cursor = 'default';
});
this.canvas.style.cursor = 'grab';
}
/**
* ABSTRACT: Führt ein empfangenes Command aus
* Muss von Subklassen überschrieben werden
* @param {Object} command - Das Command-Objekt
*/
executeCommand(command) {
throw new Error(`${this.constructor.name} must implement executeCommand()`);
}
/**
* ABSTRACT: Rendert die Visualisierung
* Muss von Subklassen überschrieben werden
*/
render() {
throw new Error(`${this.constructor.name} must implement render()`);
}
/**
* Hilfsmethode: Zeichne Overlay (Zoom, Anleitung, etc)
* Kann von Subklassen aufgerufen oder erweitert werden
* @protected
*/
drawOverlay() {
if (!this.config.showOverlay) return;
// Zoom indicator
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
this.ctx.fillRect(10, 10, 120, 30);
this.ctx.fillStyle = '#fff';
this.ctx.font = '14px Arial';
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'top';
this.ctx.fillText(`Zoom: ${(this.viewport.scale * 100).toFixed(0)}%`, 20, 18);
}
/**
* Setzt Zoom und Pan auf Standard-Werte
*/
resetView() {
this.viewport.scale = 1.0;
this.viewport.offsetX = 0;
this.viewport.offsetY = 0;
this.render();
}
/**
* Sendet ein Command an Parent-Window
* @protected
* @param {Object} message - Die Message
*/
sendToParent(message) {
if (window.parent !== window) {
window.parent.postMessage(message, '*');
}
}
/**
* Utility: Transformiere Canvas-Koordinaten zu World-Koordinaten
* Beachtet Viewport-Transformation
* @protected
* @param {number} canvasX
* @param {number} canvasY
* @returns {Object} { x, y } in World-Koordinaten
*/
canvasToWorld(canvasX, canvasY) {
return {
x: (canvasX - this.viewport.offsetX) / this.viewport.scale,
y: (canvasY - this.viewport.offsetY) / this.viewport.scale
};
}
/**
* Utility: Transformiere World-Koordinaten zu Canvas-Koordinaten
* @protected
* @param {number} worldX
* @param {number} worldY
* @returns {Object} { x, y } in Canvas-Koordinaten
*/
worldToCanvas(worldX, worldY) {
return {
x: worldX * this.viewport.scale + this.viewport.offsetX,
y: worldY * this.viewport.scale + this.viewport.offsetY
};
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = BaseVisualizer;
}