25 - Pizarra interactiva multiusuario


El último ejemplo que implementaré y utilizará JSON para la comunicación entre el cliente y el servidor será una "pizarra interactiva multiusuario", básicamente desarrollaremos una aplicación que muestre un tablero con letras que se puedan mover con el mouse. Lo interesante será que cada un cierto tiempo nos comunicaremos con el servidor e informaremos las letras que se han desplazado dentro de la ventana, esto permitirá que cualquier otro usuario que esté ejecutando en ese momento la misma página verá el desplazamiento que efectuó otra persona.

Para probar si realmente funciona esta característica podemos ejecutar el "problema resuelto" utilizando el FireFox y el Internet Explorer. Podremos observar como se sincronizan las posiciones de las letras dentro de la ventana (si un usuario mueve una letra hacia la derecha, luego de algunos segundos todos los otros usuarios verán reflejado el cambio en sus navegadores.

Veamos los distintos archivos que intervienen (pagina1.html):

<html>
<head>
<title>Problema</title>
<script src="../json.js" language="JavaScript"></script>
<script src="funciones.js" language="JavaScript"></script>
</head>
<body style="background:#eee">
<div><strong>Puede desplazar las letras con el mouse para escribir 
palabras que serán vistas por otros usuarios que visiten la página 
en este momento o más tarde.</strong></div>
<div id="letras"></div>
</body>
</html> 

Hay que tener en cuenta que emplearemos JSON para la transferencia de datos entre el cliente y el servidor por lo que debemos disponer:

<script src="../json.js" language="JavaScript"></script>

Este archivo no tiene nada de especial toda la complejidad se encuentra en el archifo funciones.js que lo incorporamos con la siguiente línea:

<script src="funciones.js" language="JavaScript"></script>

Ahora el archivo donde se encuentra toda la complejidad del código que se ejecuta en el cliente está en funciones.js:

addEvent(window,'load',inicializarEventos,false);

function desactivarSeleccion(e)
{
 return false
}


var conexion1;
function inicializarEventos()
{
  document.onmousedown=desactivarSeleccion;
  document.onmousemove=desactivarSeleccion;

  conexion1=crearXMLHttpRequest();
  conexion1.onreadystatechange = procesarEventos;
  conexion1.open('GET','pagina1.php', true);
  conexion1.send(null);
}

var datos;
var datosNuevos;
var datosMovil;
var vectorLetras=new Array();
var reloj=null;
var relojGeneral=null;
var pasos=0;
function procesarEventos()
{
  if(conexion1.readyState == 4)
  {
    datos=conexion1.responseText.parseJSON();
    crearLetras();
    relojGeneral=window.setInterval(actualizarCoordenadas, 5000);
  } 
}

function crearLetras()
{

  for(f=0;f<datos.length;f++)
  {
    var ob=document.createElement('div');
    ob.style.left=datos[f].x+'px';
    ob.style.top=datos[f].y+'px';
    ob.style.width='17px';
    ob.style.height='17px';
    ob.style.background='#eee';
    ob.style.position='absolute';
    ob.style.fontSize='18px';
    ob.style.padding='3px';
    ob.style.cursor='pointer';
    ob.id='div'+f;
    ob.style.textAlign='center';
    var x=document.getElementById('letras');
    x.appendChild(ob);
    var ref=document.getElementById('div'+f);
    ref.innerHTML=datos[f].letra;
    vectorLetras[f]=new Recuadro(ob,datos[f].letra,f+1,datos[f].x,datos[f].y);
  }
}

function letrasMovidas(cod,x,y) {
  this.codigo=cod;
  this.x=x;
  this.y=y;
}

function actualizarCoordenadas()
{
  var let=new Array();
  var con=0;
  for(f=0;f<vectorLetras.length;f++)
  {
    if (datos[f].x!=vectorLetras[f].retornarX() ||
        datos[f].y!=vectorLetras[f].retornarY())
    {
      datos[f].x=vectorLetras[f].retornarX();
      datos[f].y=vectorLetras[f].retornarY();
      let[con]=new 
         letrasMovidas(datos[f].codigo,vectorLetras[f].retornarX(),vectorLetras[f].retornarY());
      con++;
    } 
 }
  var aleatorio=Math.random();
  var cadena=let.toJSONString();
  conexion1=crearXMLHttpRequest();
  conexion1.onreadystatechange = procesarEventosContinuos;
  conexion1.open('GET','pagina2.php?letras='+cadena+"&aleatorio="+aleatorio, true);
  conexion1.send(null);
}

function procesarEventosContinuos()
{
  if(conexion1.readyState == 4)
  {
    datosNuevos=conexion1.responseText.parseJSON();
    datosMovil=conexion1.responseText.parseJSON();

    var cambios=false;
    for(f=0;f<datosNuevos.length;f++)
    {
       if (datosNuevos[f].x!=datos[f].x ||
           datosNuevos[f].y!=datos[f].y)
       {
           datosMovil[f].x=datos[f].x;
           datosMovil[f].y=datos[f].y;
           cambios=true;
       }
    } 
    if (cambios)
    {
      if (reloj==null)
        reloj=window.setInterval(moverLetras, 5);
     clearInterval(relojGeneral);
     pasos=20; 
    }
  } 
}

function moverLetras()
{
    var cambios=false;
    pasos--;
    for(f=0;f<datosNuevos.length;f++)
    {
       if (datosNuevos[f].x!=datos[f].x ||
           datosNuevos[f].y!=datos[f].y)
       {
           cambios=true;
           var dx=Math.abs(datosNuevos[f].x-datos[f].x);
           var avancex;
           if ((datosNuevos[f].x-datos[f].x)>0)
             avancex=Math.round(dx/20);
           else
             avancex=Math.round(-dx/20);
           datosMovil[f].x=parseInt(datosMovil[f].x)+avancex;
          
           var dy=Math.abs(datosNuevos[f].y-datos[f].y);
           var avancey;
           if ((datosNuevos[f].y-datos[f].y)>0)
             avancey=Math.round(dy/20);
           else
             avancey=Math.round(-dy/20);
           datosMovil[f].y=parseInt(datosMovil[f].y)+avancey;

           cambios=true;
           if (pasos==0)
           {
             vectorLetras[f].fijarX(datosNuevos[f].x);
             vectorLetras[f].fijarY(datosNuevos[f].y);
           }
           else
           {
             vectorLetras[f].fijarX(datosMovil[f].x);
             vectorLetras[f].fijarY(datosMovil[f].y);
           }
       }
    } 
    if (pasos==0)
    {
      clearInterval(reloj);
      reloj=null;
      relojGeneral=window.setInterval(actualizarCoordenadas, 5000);
    }
}


//Drag and Drop

Recuadro=function(div)
{
    tX=0;
    tY=0;
    difX=0;
    difY=0;
    addEvent(div,'mousedown',inicioDrag,false);

    function coordenadaX(e)
    {
      if (window.event)
        return event.clientX+document.body.scrollTop;
      else
        return e.pageX;
    }

    function coordenadaY(e)
    {
      if (window.event)
        return event.clientY+document.body.scrollTop;
      else
        return e.pageY;
    }

    function inicioDrag(e) 
    {

      if (window.event)
        e=window.event;
      var eX=coordenadaX(e);
      var eY=coordenadaY(e);
      var oX=parseInt(div.style.left);
      var oY=parseInt(div.style.top);
      difX=oX-eX;
      difY=oY-eY;
      addEvent(document,'mousemove',drag,false);
      addEvent(document,'mouseup',soltar,false);

    }

    function drag(e) 
    { 
      if (window.event)
        e=window.event;
      tX=coordenadaY(e)+difY+'px';
      tY=coordenadaX(e)+difX+'px'
      div.style.top=tX; 
      div.style.left=tY; 
    }
  

    function soltar(e)
    { 
      if (window.event)
      {
        document.detachEvent('onmousemove',drag);
        document.detachEvent('onmouseup',soltar);
      }   
      else
      {
        document.removeEventListener('mousemove',drag,false);
        document.removeEventListener('mouseup',soltar,false);
      }
      actualizarCoordenadas();
    }

    this.retornarX=function()
    {
      return parseInt(div.style.left);
    }
    
    this.retornarY=function()
    {
      return parseInt(div.style.top);
    }

    this.fijarX=function(xx)
    {
      div.style.left=xx+'px'; 
    }
    
    this.fijarY=function(yy)
    {
      div.style.top=yy+'px'; 
    }

}

//***************************************
//Funciones comunes a todos los problemas
//***************************************
function addEvent(elemento,nomevento,funcion,captura)
{
  if (elemento.attachEvent)
  {
    elemento.attachEvent('on'+nomevento,funcion);
    return true;
  }
  else  
    if (elemento.addEventListener)
    {
      elemento.addEventListener(nomevento,funcion,captura);
      return true;
    }
    else
      return false;
}

function crearXMLHttpRequest() 
{
  var xmlHttp=null;
  if (window.ActiveXObject) 
    xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
  else 
    if (window.XMLHttpRequest) 
      xmlHttp = new XMLHttpRequest();
  return xmlHttp;
}

La primera función que se ejecuta es inicializarEventos donde tenemos:

  document.onmousedown=desactivarSeleccion;
  document.onmousemove=desactivarSeleccion;

Con estas dos asignaciones desactivamos la posibilidad de seleccionar texto dentro de la página, esto es para que no se puedan seleccionar las letras.

Luego:

  conexion1=crearXMLHttpRequest();
  conexion1.onreadystatechange = procesarEventos;
  conexion1.open('GET','pagina1.php', true);
  conexion1.send(null);

creamos un objeto de la clase crearXMLHttpRequest para recuperar la posición de cada letra. Veremos más adelante que tenemos una base de datos donde almacenamos las letras y las coordenadas de cada una.

La funcion procesarEventos:

function procesarEventos()
{
  if(conexion1.readyState == 4)
  {
    datos=conexion1.responseText.parseJSON();
    crearLetras();
    relojGeneral=window.setInterval(actualizarCoordenadas, 5000);
  } 
}

cuando los datos se han enviado por completo del servidor procedemos a rescatarlos y generar un objeto literal en JavaScript llamando a la función parseJSON().
Llamamos seguidamente a la función crearLetras() y finalmente creamos un timer o alarma para que se dispare cada 5 segundos, veremos luego que tiene por objetivo recuperar las coordenadas de las letras almacenadas en el servidor:

    relojGeneral=window.setInterval(actualizarCoordenadas, 5000);

La función crearLetras:

function crearLetras()
{

  for(f=0;f<datos.length;f++)
  {
    var ob=document.createElement('div');
    ob.style.left=datos[f].x+'px';
    ob.style.top=datos[f].y+'px';
    ob.style.width='17px';
    ob.style.height='17px';
    ob.style.background='#eee';
    ob.style.position='absolute';
    ob.style.fontSize='18px';
    ob.style.padding='3px';
    ob.style.cursor='pointer';
    ob.id='div'+f;
    ob.style.textAlign='center';
    var x=document.getElementById('letras');
    x.appendChild(ob);
    var ref=document.getElementById('div'+f);
    ref.innerHTML=datos[f].letra;
    vectorLetras[f]=new Recuadro(ob,datos[f].letra,f+1,datos[f].x,datos[f].y);
  }
}

crea elementos HTML de tipo "div" y los dispone en las coordenadas que acabamos de recuperar del servidor:

    ob.style.left=datos[f].x+'px';
    ob.style.top=datos[f].y+'px';

El ancho y el alto son fijos:

    ob.style.width='17px';
    ob.style.height='17px';

Definimos un id distinto a cada uno:

    ob.id='div'+f;

Por último lo añadimos a la página:

    ref.innerHTML=datos[f].letra;
    vectorLetras[f]=new Recuadro(ob,datos[f].letra,f+1,datos[f].x,datos[f].y);

y creamos un objeto de la clase Recuadro que nos permitirá desplazarlo con el mouse (esta clase se estudió en el curso de DHTML Ya.

La función actualizarCoordenadas se dispara cada 5 segundos o inmediatamente después que un usuario desplaza una letra en la pantalla:

function actualizarCoordenadas()
{
  var let=new Array();
  var con=0;
  for(f=0;f<vectorLetras.length;f++)
  {
    if (datos[f].x!=vectorLetras[f].retornarX() ||
        datos[f].y!=vectorLetras[f].retornarY())
    {
      datos[f].x=vectorLetras[f].retornarX();
      datos[f].y=vectorLetras[f].retornarY();
      let[con]=new 
         letrasMovidas(datos[f].codigo,vectorLetras[f].retornarX(),vectorLetras[f].retornarY());
      con++;
    } 
 }
  var aleatorio=Math.random();
  var cadena=let.toJSONString();
  conexion1=crearXMLHttpRequest();
  conexion1.onreadystatechange = procesarEventosContinuos;
  conexion1.open('GET','pagina2.php?letras='+cadena+"&aleatorio="+aleatorio, true);
  conexion1.send(null);
}

Dentro del for identificamos si alguna de las letras fue desplazada con el mouse:

    if (datos[f].x!=vectorLetras[f].retornarX() ||
        datos[f].y!=vectorLetras[f].retornarY())

En caso afirmativo actualizamos la estructura datos:

      datos[f].x=vectorLetras[f].retornarX();
      datos[f].y=vectorLetras[f].retornarY();

y además creamos una componente del a clase letrasMoviles:

     let[con]=new 
         letrasMovidas(datos[f].codigo,vectorLetras[f].retornarX(),vectorLetras[f].retornarY());

Este vector let tiene los cambios efectuados en pantalla para ser eviados al servidor.

Fuera del for creamos un objeto de la clase XMLHttpRequest y procedemos a enviar los datos al servidor:

  conexion1.open('GET','pagina2.php?letras='+cadena+"&aleatorio="+aleatorio, true);

Recordemos que para convertir el vector de JavaScript a JSON lo hacemos:

  var cadena=let.toJSONString();

La función procesarEventosContinuos:

function procesarEventosContinuos()
{
  if(conexion1.readyState == 4)
  {
    datosNuevos=conexion1.responseText.parseJSON();
    datosMovil=conexion1.responseText.parseJSON();

    var cambios=false;
    for(f=0;f<datosNuevos.length;f++)
    {
       if (datosNuevos[f].x!=datos[f].x ||
           datosNuevos[f].y!=datos[f].y)
       {
           datosMovil[f].x=datos[f].x;
           datosMovil[f].y=datos[f].y;
           cambios=true;
       }
    } 
    if (cambios)
    {
      if (reloj==null)
        reloj=window.setInterval(moverLetras, 5);
     clearInterval(relojGeneral);
     pasos=20; 
    }
  } 
}

Recupera las coordenadas actuales de las letras que se encuentran registradas en el servidor:

    datosNuevos=conexion1.responseText.parseJSON();
    datosMovil=conexion1.responseText.parseJSON();

Utilizamos dos variables ya que una la utilizaremos para ir desplazando lentamente la letra por la pantalla.

Dentro de un for verificamos si hay coordenadas distintas entre las que administra nuestro navegador y las registradas en el servidor:

       if (datosNuevos[f].x!=datos[f].x ||
           datosNuevos[f].y!=datos[f].y)
       {

En caso de haber diferencias:

    if (cambios)
    {
      if (reloj==null)
        reloj=window.setInterval(moverLetras, 5);
     clearInterval(relojGeneral);
     pasos=20; 
    }

desactivamos el timer relojGeneral y activamos un timer para desplazar lentamente las letras entre la posición actual y la registrada en el servidor (la función moverLetras se dispara cada 5 milisegundos.

La función moverLetra:

function moverLetras()
{
    var cambios=false;
    pasos--;
    for(f=0;f<datosNuevos.length;f++)
    {
       if (datosNuevos[f].x!=datos[f].x ||
           datosNuevos[f].y!=datos[f].y)
       {
           cambios=true;
           var dx=Math.abs(datosNuevos[f].x-datos[f].x);
           var avancex;
           if ((datosNuevos[f].x-datos[f].x)>0)
             avancex=Math.round(dx/20);
           else
             avancex=Math.round(-dx/20);
           datosMovil[f].x=parseInt(datosMovil[f].x)+avancex;
          
           var dy=Math.abs(datosNuevos[f].y-datos[f].y);
           var avancey;
           if ((datosNuevos[f].y-datos[f].y)>0)
             avancey=Math.round(dy/20);
           else
             avancey=Math.round(-dy/20);
           datosMovil[f].y=parseInt(datosMovil[f].y)+avancey;

           cambios=true;
           if (pasos==0)
           {
             vectorLetras[f].fijarX(datosNuevos[f].x);
             vectorLetras[f].fijarY(datosNuevos[f].y);
           }
           else
           {
             vectorLetras[f].fijarX(datosMovil[f].x);
             vectorLetras[f].fijarY(datosMovil[f].y);
           }
       }
    } 
    if (pasos==0)
    {
      clearInterval(reloj);
      reloj=null;
      relojGeneral=window.setInterval(actualizarCoordenadas, 5000);
    }
}

desplaza las letras que han cambiado de posición. Esta función se ejecuta 20 veces hasta que la variable global pasos almacene el valor 0.


Luego tenemos los dos archivos que se ejecutan en el servidor (pagina1.php):

<?php
$conexion=mysql_connect("localhost","root","z80") 
  or die("Problemas en la conexion");
mysql_select_db("bdajax",$conexion) or die("Problemas en la seleccion
  de la base de datos");
$registros=mysql_query("select letra,x,y,codigo from letras",$conexion) 
  or die("Problemas en el select".mysql_error());

while ($reg=mysql_fetch_array($registros))
{
  $vec[]=$reg;
}
mysql_close($conexion);
require('../JSON.php');
$json=new Services_JSON();
$cad=$json->encode($vec);
echo $cad;
?>

Recupera de la tabla letras las coordenadas y letras propiamente dichas que serán mostradas en el servidor:

$registros=mysql_query("select letra,x,y,codigo from letras",$conexion) 
  or die("Problemas en el select".mysql_error());

Guardamos los datos en un vector:

while ($reg=mysql_fetch_array($registros))
{
  $vec[]=$reg;
}

Generamos un archivo con formato JSON para que se envíe al cliente:

require('../JSON.php');
$json=new Services_JSON();
$cad=$json->encode($vec);
echo $cad;

Por último nos queda el archivo que llamamos cada 5 segundos para indicarle las novedades dentro del navegador (si el usuario desplazó alguna letra) y recuperar las novedades registradas en el servidor:

<?php
require('../JSON.php');
$json=new Services_JSON();
$cad=$json->decode(stripSlashes($_REQUEST['letras']));
$conexion=mysql_connect("localhost","root","z80") 
  or die("Problemas en la conexion");
mysql_select_db("bdajax",$conexion) or die("Problemas en la seleccion
  de la base de datos");
$registros=mysql_query("select letra,x,y,codigo from letras",$conexion) 
  or die("Problemas en el select".mysql_error());
for($f=0;$f<count($cad);$f++)
{
  mysql_query("update letras set x=".$cad[$f]->x.",y=".$cad[$f]->y." 
    where codigo=".$cad[$f]->codigo,$conexion) or die("Problemas en el 
  select".mysql_error());
}
$registros=mysql_query("select x,y,codigo from letras",$conexion) or 
  die("Problemas en el select".mysql_error());
while ($reg=mysql_fetch_array($registros))
{
  $vec[]=$reg;
}
mysql_close($conexion);
$json=new Services_JSON();
$cad=$json->encode($vec);
echo $cad;
?>

Primero recuperamos los datos enviados por el navegador y generamos un vector asociativo en PHP a partir de los datos que llegan en formato JSON:

$cad=$json->decode(stripSlashes($_REQUEST['letras']));

Modificamos las coordenadas de las letras:

$registros=mysql_query("select letra,x,y,codigo from letras",$conexion) 
  or die("Problemas en el select".mysql_error());
for($f=0;$f<count($cad);$f++)
{
  mysql_query("update letras set x=".$cad[$f]->x.",y=".$cad[$f]->y." 
    where codigo=".$cad[$f]->codigo,$conexion) or die("Problemas en el 
  select".mysql_error());
}

Por último recuperamos todas las letras y sus coordenadas y las enviamos nuevamente al cliente (navegador) que las solicitó:

$registros=mysql_query("select x,y,codigo from letras",$conexion) or 
  die("Problemas en el select".mysql_error());
while ($reg=mysql_fetch_array($registros))
{
  $vec[]=$reg;
}
mysql_close($conexion);
$json=new Services_JSON();
$cad=$json->encode($vec);
echo $cad;