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 Chrome. 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):

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Problema</title>
  <script src="funciones.js"></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>

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

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

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

addEventListener('load',inicializarEventos,false);

function desactivarSeleccion(e)
{
 return false
}


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

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

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=JSON.parse(conexion1.responseText);
    crearLetras();
    relojGeneral=window.setInterval(actualizarCoordenadas, 5000);
  } 
}

function crearLetras()
{

  for(var 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 vecletras=new Array();
  var con=0;
  for(var 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();
      vecletras[con]=new letrasMovidas(datos[f].codigo,vectorLetras[f].retornarX(),vectorLetras[f].retornarY());
      con++;
    } 
 }
  var aleatorio=Math.random();
  var cadena=JSON.stringify(vecletras);
  conexion1=new XMLHttpRequest();
  conexion1.onreadystatechange = procesarEventosContinuos;
  conexion1.open('GET','pagina2.php?letras='+cadena+"&aleatorio="+aleatorio, true);
  conexion1.send();

}

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

    var cambios=false;
    for(var 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(var 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;
    div.addEventListener('mousedown',inicioDrag,false);

    function coordenadaX(e)
    {
        return e.pageX;
    }

    function coordenadaY(e)
    {
        return e.pageY;
    }

    function inicioDrag(e) 
    {
      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;
      document.addEventListener('mousemove',drag,false);
      document.addEventListener('mouseup',soltar,false);

    }

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

    function soltar(e)
    { 
      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'; 
    }

}

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=new XMLHttpRequest();
  conexion1.onreadystatechange = procesarEventos;
  conexion1.open('GET','pagina1.php', true);
  conexion1.send();

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=JSON.parse(conexion1.responseText);
    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 parse del objeto JSON.
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(var 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 vecletras=new Array();
  var con=0;
  for(var 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();
      vecletras[con]=new letrasMovidas(datos[f].codigo,vectorLetras[f].retornarX(),vectorLetras[f].retornarY());
      con++;
    } 
  }
  var aleatorio=Math.random();
  var cadena=JSON.stringify(vecletras);
  conexion1=new XMLHttpRequest();
  conexion1.onreadystatechange = procesarEventosContinuos;
  conexion1.open('GET','pagina2.php?letras='+cadena+"&aleatorio="+aleatorio, true);
  conexion1.send();

}

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 vecletras 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=JSON.stringify(vecletras);

La función procesarEventosContinuos:

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

    var cambios=false;
    for(var 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=JSON.parse(conexion1.responseText);
    datosMovil=JSON.parse(conexion1.responseText);

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(var 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=mysqli_connect("localhost","root","","bdajax") or
    die("Problemas con la conexión");
  
$registros=mysqli_query($conexion,"select letra,x,y,codigo from letras") or die("Problemas en el select".mysqli_error($conexion));

while ($reg=mysqli_fetch_array($registros))
{
  $vec[]=$reg;
}
mysqli_close($conexion);

$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=mysqli_query($conexion,"select letra,x,y,codigo from letras") or die("Problemas en el select".mysqli_error($conexion));

Guardamos los datos en un vector:

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

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

$cad=json_encode ($vec);
echo $cad;

Por último nos queda el archivo (pagina2.php) 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

$cad=json_decode(stripslashes($_REQUEST['letras']));

$conexion=mysqli_connect("localhost","root","","bdajax") or
    die("Problemas con la conexión");

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

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

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

while ($reg=mysqli_fetch_array($registros))
{
  $vec[]=$reg;
}
mysqli_close($conexion);
$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=mysqli_query($conexion,"select letra,x,y,codigo from letras") or die("Problemas en el select".mysqli_error($conexion));

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

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

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

while ($reg=mysqli_fetch_array($registros))
{
  $vec[]=$reg;
}
mysqli_close($conexion);
$cad=json_encode ($vec);
echo $cad;


Problema resuelto.


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 ejecutarlo con el FireFox y el Chrome en forma simultánea. 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)

Si lo ejecuta en dos instancias de navegadores debe quedar en claro que cada uno se comunica con el servidor en forma independiente para enviar y recuperar los cambios de posición.


pagina1.html



Ejecutar ejemplo



funciones.js




pagina1.php




pagina2.php


Retornar