/**
* @file render.js Implementación del render del MM
* @author José Luis Molina Soria
* @version 20130807
*/
/**
* @class MM.Render
* @classdesc Render de MM. Capaz de renderizar un MM completo
* @constructor MM.Render
* @param {Element} contenedor Elemento DOM donde renderizar el MM
* @param {MM.NodoSimple|MM.Globo} claseNodo Clase de renderizado de Nodos a utilizar.
* Por defecto utiliza la clase MM.Globo
* @param {MM.Arista|MM.Rama} claseArista Clase de renderizado de aristas a utilizar.
* Por defecto utiliza la clase MM.Arista
*/
MM.Render = function() {
var render = MM.Class.extend(/** @lends MM.Render.prototype */{
init : function (contenedor, claseNodo, claseArista) {
/** @prop {Element} contenedor Elemento DOM. Contenedor del escenario */
this.contenedor = window.document.getElementById(contenedor);
/** @prop {number} width Ancho en pixeles del MM. Calculado a partir del contenedor */
this.width = this.contenedor.clientWidth - 2; // -2px
/** @prop {number} height Alto en pixeles del MM. Calculado a partir del contenedor */
this.height = this.contenedor.clientHeight - 2; // -2px
/** @prop {number} devicePixelRatio Pixel Ratio del dispositivo. */
this.devicePixelRatio = getDevicePixelRatio();
/** @prop {MM.Globo|MM.NodoSimple} Nodo Clase de renderizado de nodos. Por defecto, MM.Globo */
this.Nodo = claseNodo || MM.Globo;
/** @prop {MM.Arista|MM.Rama} Arista Clase de renderizado de aristas. Por defecto, MM.Arista */
this.Arista = claseArista || MM.Arista;
/** @prop {Kinetic.Stage} escenario Escenario donde irán cubicadas las capas de dibujo (Layers | Canvas). */
this.escenario = new Kinetic.Stage({
container: contenedor,
width: this.width,
height: this.height,
draggable: true,
dragBoundFunc: function (pos) {
MM.render.offset = pos;
return pos;
}
});
//this.escenario.on('dragend', MM.Class.bind(this, this.dibujar) );
this.offset = {x:0, y:0};
/** @prop {Kinetic.Layer} capaGrid Capa donde se dibujará el grid o rejilla del MM */
this.capaGrid = new Kinetic.Layer();
/** @prop {Kinetic.Layer} capaNodos Capa donde se dibujarán los nodos del MM */
this.capaNodos = new Kinetic.Layer();
/** @prop {Kinetic.Layer} capaAristas Capa donde se dibujarán las aristas del MM */
this.capaAristas = new Kinetic.Layer();
this.capaTransparencia = new Kinetic.Layer({visible : false});
this.escenario.add(this.capaGrid);
this.escenario.add(this.capaAristas);
this.escenario.add(this.capaNodos);
this.escenario.add(this.capaTransparencia);
}
});
/** @prop {Array} aristas Conjunto de aristas (MM.Arista o MM.Rama) renderizadas en el MM */
render.prototype.aristas = [];
/** @prop {Array} suscripciones Array de id de suscripciones (id de eventos) */
render.prototype.suscripciones = [];
/**
* @desc Método encargado de realizar el renderizado del MM.
* @memberof MM.Render
* @method renderizar
* @instance
*/
render.prototype.renderizar = function () {
this.capaGrid.removeChildren();
new MM.Grid(this.capaGrid, this.width, this.height);
this.dibujar ( MM.arbol);
this.suscribrirEventos();
MM.root();
MM.definirAtajos();
};
var numLineas = function (texto) {
return texto.split("\n").length;
};
var minimaAlturaNodo = function ( nodo ) {
return 35 + (numLineas(nodo.elemento.texto)-1) * 15;
};
render.prototype.calcularAlturas = function (arbol) {
var minAltura = function (a) {
if ( a.esHoja() || a.elemento.plegado ) {
return minimaAlturaNodo(a);
}
var p = 0;
a.hijos.forEach(function (h) {
p = p + minAltura(h);
});
return p;
};
var altura = 0;
if ( arbol.elemento.id === MM.arbol.elemento.id ) {
altura = minAltura(arbol);
altura = ( this.height <= altura ) ? altura : this.height / this.getEscala();
} else {
altura = arbol.elemento.reparto.y1 - arbol.elemento.reparto.y0;
}
var alturaHijos = arbol.hijos.map(minAltura);
var suma = alturaHijos.reduce ( function(ac, x) { return ac + x; }, 0 );
var division = (altura - suma) / arbol.hijos.length;
alturaHijos = alturaHijos.map(function(x) { return x+division; });
minAltura = suma = division = null;
return { padre: altura, hijos: alturaHijos };
};
/**
* @desc Dibuja el MindMap a partir del estado actual del árbol.
* @memberof MM.Render
* @method dibujar
* @instance
*/
render.prototype.dibujar = function (arbol) {
console.debug ('dibujar ' + arbol.elemento.texto );
var idSusPre = arbol.suscribir('preOrden', MM.Class.bind(this, preRecorrido) );
var idSusPost = arbol.suscribir('postPreOrden', MM.Class.bind(this, postRecorrido) );
arbol.preOrden();
arbol.desSuscribir(idSusPre);
arbol.desSuscribir(idSusPost);
arbol = idSusPre = idSusPost = null;
this.escenario.draw();
};
var preRecorrido = function (nodo) {
this.repartoEspacio(nodo);
};
var postRecorrido = function (nodo) {
var elemento = nodo.elemento;
nodo.hijos.forEach(function (hijo) {
var arista = this.buscarArista(nodo, hijo);
if ( arista === null ) {
arista = new this.Arista(this.capaAristas, elemento, hijo.elemento, '3');
this.aristas.push(arista);
} else {
this.aristas[arista].redraw();
}
arista = null;
}, this);
elemento = null;
};
/**
* @desc Se encarga de repartir el espacio entre los nodos hijos de un nodo padre dado.
* Cada Nodo tiene un espacio asignado en el que puede ser renderizado.
* @param {MM.Arbol} arbol Nodo padre de los nodos que deseamos organizar
* @memberof MM.Render
* @method repartoEspacio
* @inner
*/
render.prototype.repartoEspacio = function (arbol) {
var reparto = arbol.elemento.reparto;
var alturas = this.calcularAlturas (arbol);
if ( arbol.elemento.id === MM.arbol.elemento.id ) {
arbol.elemento.reparto = reparto = {y0: 0, y1: alturas.padre,
xPadre : 0, widthPadre: 0 };
this.posicionarNodo ( arbol );
}
var y0 = reparto.y0;
var widthPadre = arbol.elemento.nodo.getWidth();
var xPadre = arbol.elemento.nodo.getX();
arbol.hijos.forEach(function (hijo, i) {
hijo.elemento.reparto = {y0: y0, y1: y0 + alturas.hijos[i],
xPadre : xPadre, widthPadre: widthPadre };
this.posicionarNodo ( hijo );
y0 += alturas.hijos[i];
}, this);
reparto = y0 = xPadre = widthPadre = null;
};
/**
* @desc Posiciona un nodo del arbol en función de la profundidad. Si el nodo no
* esta renderizado lo renderiza dentro del espacio asignado para él.
* @param {MM.Arbol} arbol Nodo del arbol que deseamos prosicionar
* @memberof MM.Render
* @method posicionarNodo
* @inner
*/
render.prototype.posicionarNodo = function (arbol) {
var elemento = arbol.elemento;
var reparto = elemento.reparto;
var visible = true;
var x = 20;
if ( arbol.elemento.id !== MM.arbol.elemento.id ) {
x = reparto.xPadre + reparto.widthPadre + 75;
var padre = MM.arbol.padreDe(elemento.id);
if ( padre ) {
visible = !(padre.elemento.plegado && arbol.elemento.plegado);
} else {
visible = !arbol.elemento.plegado;
}
}
var y = reparto.y0 + ( (reparto.y1 - reparto.y0) / 2) - (minimaAlturaNodo(arbol) / 2);
if (elemento.nodo === null) {
elemento.nodo = new this.Nodo(this, arbol, { x: x, y: y, text: elemento.texto});
}
y = reparto.y0 + ( (reparto.y1 - reparto.y0) / 2) - (elemento.nodo.getHeight() / 2);
elemento.nodo.setX(x);
elemento.nodo.setY(y);
elemento.nodo.setVisible(visible);
elemento = reparto = x = y = null;
};
/**
* @desc Método que se encarga de realizar y registrar las suscripciones a eventos del MM.
* @memberof MM.Render
* @method suscribirEventos
* @instance
*/
render.prototype.suscribrirEventos = function ( ) {
this.desuscribrirEventos(); // evitamos dobles suscripciones
var sus = this.suscripciones;
var e = MM.eventos;
sus.push ( e.suscribir('ponerFoco', cambiarFoco) );
sus.push ( e.suscribir('add', this.nuevoNodo, this) );
sus.push ( e.suscribir('borrar', this.borrarNodo, this) );
sus.push ( e.suscribir('nuevo/pre', function () {
MM.arbol.elemento.nodo.destroy();
}) );
sus.push ( e.suscribir('nuevo/post', function () {
this.renderizar();
}, this) );
this.contenedor.addEventListener("mousewheel", handlerWheel, false);
this.contenedor.addEventListener("DOMMouseScroll", handlerWheel, false);
sus = e = null;
};
/**
* @desc Borra las suscriciones a eventos del MM.
* @memberof MM.Render
* @method desuscribirEventos
* @instance
*/
render.prototype.desuscribrirEventos = function ( ) {
this.suscripciones.forEach ( function ( idSus ) {
MM.eventos.desSuscribir(idSus);
});
this.suscripciones = [];
this.contenedor.removeEventListener("mousewheel", handlerWheel);
this.contenedor.removeEventListener("DOMMouseScroll", handlerWheel);
};
/**
* @desc Renderiza las aristas de forma independiente
* @memberof MM.Render
* @method renderAristas
* @inner
*/
render.prototype.renderAristas = function () {
if (!this.capaAristas) { return; }
this.aristas.forEach(function (arista) {
arista.redraw();
});
this.capaAristas.draw();
};
/**
* @desc Renderiza un nuevo nodo. Es lanzado en el momento de crear un nuevo nodo en el MM.
* Es decir, atiende al evento del MM de creación de nuevos nodos
* @param {MM.Arbol} padre Nodo padre del nuevo nodo
* @param {MM.Arbol} hijo Nodo nuevo. Nodo a renderizar
* @memberof MM.Render
* @method nuevoNodo
* @inner
*/
render.prototype.nuevoNodo = function (padre, hijo) {
this.repartoEspacio(padre);
this.aristas.push(new this.Arista(this.capaAristas, padre.elemento, hijo.elemento, '3'));
this.dibujar(padre);
MM.ponerFoco(hijo);
};
var getDevicePixelRatio = function () {
if ( window.devicePixelRatio ) {
return window.devicePixelRatio;
}
return 1;
};
/**
* @desc Buscador de aristas en función del padre e hijo (origen - destino).
* @param {MM.Arbol} padre Padre o nodo origen de la arista
* @param {MM.Arbol} hijo Hijo o nodo destino de la arista
* @memberof MM.Render
* @method buscarArista
* @inner
*/
render.prototype.buscarArista = function (padre, hijo) {
var a;
for (var i = 0; i < this.aristas.length; i++) {
a = this.aristas[i];
if (padre.elemento.id === a.elementoOrigen.id && hijo.elemento.id === a.elementoDestino.id) {
return i;
}
}
a = null;
return null;
};
/**
* @desc Eliminar una arista del conjunto de aristas del render
* @param {MM.Arbol} padre Padre o nodo origen de la arista
* @param {MM.Arbol} hijo Hijo o nodo destino de la arista
* @memberof MM.Render
* @method borrarArista
* @inner
*/
render.prototype.borrarArista = function (padre, hijo) {
var a;
for (var i = 0; i < this.aristas.length; i++) {
a = this.aristas[i];
if (padre.elemento.id === a.elementoOrigen.id && hijo.elemento.id === a.elementoDestino.id) {
a.destroy();
return this.aristas.splice(i, 1);
}
}
a = null;
return null;
};
/**
* @desc Borra un nodo hijo.
* @param {MM.Arbol} padre Nodo padre del elemento a borrar
* @param {MM.Arbol} hijo Nodo a borrar.
* @memberof MM.Render
* @method borrarHijo
* @inner
*/
render.prototype.borrarHijo = function (padre, hijo) {
for (var i = 0; i < padre.hijos.length; i++) {
if (padre.hijos[i].elemento.id === hijo.elemento.id) {
return padre.hijos.splice(i, 1);
}
}
return null;
};
/**
* @desc Borra un nodo. Manejador del evento de borrado de nodos del MM.
* @param {MM.Arbol} padre Nodo padre del elemento a borrar
* @param {MM.Arbol} borrado Nodo a borrar.
* @memberof MM.Render
* @method borrarNodo
* @inner
*/
render.prototype.borrarNodo = function (padre, borrado) {
// recorremos los hijos. i no incrementa por que después de borrar queda un elemento menos
for (var i = 0; i < borrado.hijos.length; i) {
this.borrarNodo(borrado, borrado.hijos[i]);
}
// borramos los elementos gráficos relacionados
this.borrarArista(padre, borrado);
borrado.elemento.nodo.destroy();
// importante borrar el hijo borrado para evitar errores en el pintado
this.borrarHijo(padre, borrado);
this.dibujar(padre);
i = null;
};
/**
* @desc Calcula la escala a la que esta renderizada la imagen
* @return {number} Escala actual.
* @memberof MM.Render
* @method getEscala
* @inner
*/
render.prototype.getEscala = function () {
var scale = MM.render.escenario.getScale();
return scale.x;
};
/**
* @desc Establece la escala a la que esta renderizada la imagen
* @param {number} escala Nueva escala.
* @memberof MM.Render
* @method setEscala
* @inner
*/
render.prototype.setEscala = function ( escala ) {
MM.render.escenario.setScale({x:escala, y:escala});
MM.render.escenario.draw();
};
/**
* @desc Realiza un zoomIn al Mapa mental.
* @memberof MM.Render
* @method zoomIn
* @inner
*/
render.prototype.zoomIn = function () {
var scale = MM.render.getEscala();
MM.render.setEscala(scale+0.05);
MM.undoManager.add ( new MM.comandos.Zoom(scale, scale+0.05) );
};
/**
* @desc Realiza un zoomOut al Mapa mental.
* @memberof MM.Render
* @method zoomOut
* @inner
*/
render.prototype.zoomOut = function () {
var scale = MM.render.getEscala();
if ( scale >= 0.05 ) {
MM.render.setEscala(scale - 0.05);
MM.undoManager.add(new MM.comandos.Zoom(scale, scale-0.05) );
}
};
/**
* @desc Reseet del zoom. Establece la escala a 1.
* @memberof MM.Render
* @method zoomReset
* @inner
*/
render.prototype.zoomReset = function () {
MM.render.setEscala(1);
MM.undoManager.add(new MM.comandos.Zoom(MM.render.getEscala(), 1) );
};
/**
* @desc Cambia el foco de posición (nodo). Manejador del evento de cambio de foco del MM.
* @param {MM.Arbol} anterior Nodo que tiene el foco
* @param {MM.Arbol} siguiente Nodo que toma el foco
* @memberof MM.Render
* @method cambiarFoco
* @inner
*/
var cambiarFoco = function (anterior, siguiente) {
if ( enEdicion ) {
MM.render.editar();
}
if ( anterior !== null && anterior.elemento.nodo !== null ) {
anterior.elemento.nodo.quitarFoco();
}
if ( siguiente !== null && siguiente.elemento.nodo !== null ) {
siguiente.elemento.nodo.ponerFoco();
}
};
/**
* @desc Entra y sale de modo de edición.
* @memberof MM.Render
* @method editar
* @inner
*/
var enEdicion = false;
render.prototype.editar = function () {
var t = MM.render.capaTransparencia.canvas.element;
if ( enEdicion ) {
enEdicion = false;
t.style.background = 'transparent';
t.style.opacity = 0;
t.style.display = 'none';
MM.foco.elemento.nodo.cerrarEdicion();
} else {
enEdicion = true;
MM.foco.elemento.nodo.editar();
t.style.background = 'white';
t.style.opacity = 0.5;
t.style.display = 'block';
}
MM.atajosEnEdicion ( enEdicion );
t = null;
};
/**
* @desc Indicar si el nodo actual
* @memberof MM.Render
* @return Devuelve true cuando el nodo actual ha entrado en modo edición y false en otro caso.
* @method modoEdicion
* @inner
*/
render.prototype.modoEdicion = function() {
return enEdicion;
};
render.prototype.insertarSaltoDeLinea = function () {
if ( enEdicion ) {
var editor = MM.foco.elemento.nodo.editor;
editor.value = editor.value + "\n";
MM.foco.elemento.nodo.setTamanoEditor();
editor = null;
}
};
var handlerWheel = function (e) {
var positivo = (e.wheelDelta || -e.detail) > 0;
if ( positivo ) { // rueda hacia delante
MM.render.zoomIn();
} else { // rueda hacia atrás
MM.render.zoomOut();
}
};
return render;
}();