viernes, 8 de marzo de 2013

Como detectar burdamente que se ha dibujado una X sobre un JPanel.

Supongamos que queremos detectar si alguien dibuja sobre una pantalla táctil una X (Si utilizamos un ratón entendemos que dibujamos un trazo mientras mantenemos pulsado uno de sus botones).

Lo primero que tenemos que hacer es decidir cómo detectar si estamos dibujando el primer o segundo trazo de la X. Aunque se puede hacer de muchas maneras en este caso utilizaremos un intervalo de tiempo. Se dibuja el primer trazo (se pulsa, se arrastra y se suelta el botón)  y se espera al segundo trazo. Si se tarda mucho en hacer el segundo trazo entendemos que no estamos "escribiendo" una X si no que hemos hecho una raya y un rato después otra.

De cada trazo nos quedaremos sólo con el primer y último punto .

Para hacer lo anterior, en un JPanel, simplemente tenemos que agregarle un MouseListener para procesar los eventos del ratón.
Si se produce un evento pressed o entered llamaremos al método:

private void pressedOEntered(MouseEvent e) {
long tiempo = (System.nanoTime() - tiempoPrimeraRecta)/1000000;
System.out.println("Tiempo: " + tiempo);
if (tiempo > MAX_TIEMPO_ENTRE_RECTAS) {
// Es la primera recta
x0 = e.getX();
y0 = e.getY();
primeraRecta = true;
System.out.println("x0, y0: " + x0 + ", " + y0);
}
else {
// Es la segunda recta
x0p = e.getX();
y0p = e.getY();
primeraRecta = false;
System.out.println("x0p, y0p: " + x0p + ", " + y0p);
}
}

Si se produce un evento released o exited llamaremos al método:

private void releasedOExited(MouseEvent e) {
if (primeraRecta) {
x1 = e.getX();
y1 = e.getY();
tiempoPrimeraRecta = System.nanoTime();
System.out.println("x1, y1: " + x1 + ", " + y1);
}
else {
x1p = e.getX();
y1p = e.getY();
System.out.println("x1p, y1p: " + x1p + ", " + y1p);
comprobarX();
}
}

En el momento que se finaliza el segundo trazo llamaremos al método que comprueba si es o no una X. La comprobación que realizamos es la siguiente:
  • Son dos rectas con pendientes de signo distinto. No permitimos una recta totalmente vertical (pendiente infinita) ni X con rectas con la misma pendiente:

  • El punto de corte pertenece al dominio de los dos trazados:


La pendiente de una recta es su tangente:
El método que las calcula para nuestras rectas es:

private double getPendiente(double x0, double y0, double x1, double y1) {
   double pendiente;
if (x0 < x1) 
pendiente = (y1-y0)/(x1-x0);
else if (x0 > x1)
pendiente = (y0-y1)/(x0-x1);
else
throw new IllegalArgumentException("Pendiente infinita");
return pendiente;
}

Calculamos el punto b de cada recta partiendo de que ya tenemos las pendientes: y = mx + b => b = y – mx.

Una vez calculadas las rectas resolvemos el sistema de ecuaciones para calcular el punto de corte:

Quedando el método:

private void comprobarX() {
try {
double m = getPendiente(x0,y0,x1,y1);
System.out.println("Pendiente 1: " + m);
double mp = getPendiente(x0p,y0p,x1p,y1p);
System.out.println("Pendiente 2: " + mp);
if (m * mp >0)
System.out.println("No es X, pendientes del mismo signo");
else {
// Tenemos que ver si se cortan, y=mx+b => b = y-mx => b = y0-m * x0
double b = y0 - m * x0;
double bp = y0p - mp * x0p;
// y = mx + b, y=mp x + bp => mx+b = mp x + bp => x = (bp - b)/(m-mp), y= (m bp - mp b) / (m - mp)
double corteX = (bp - b)/(m - mp);
double corteY = (m * bp - mp * b) / (m - mp);
System.out.println("Punto de corte: " + corteX + ", " + corteY);
// Vemos si el punto de corte esta dentro del dominio de los segmentos trazados
if ( (Math.min(x0,x1) < corteX && Math.max(x0,x1) > corteX) && (Math.min(x0p, x1p) < corteX && Math.max(x0p,x1p)> corteX) && (Math.min(y0,y1) < corteY && Math.max(y0,y1) > corteY) && (Math.min(y0p, y1p) < corteY && Math.max(y0p,y1p)> corteY)) {
System.out.println("¡¡¡¡¡ Es x !!!!");
}
else 
System.out.println("No es x");
}
}
catch(IllegalArgumentException iae) {
// No vale;
System.out.print("Pendiente infinita, no vale como X");
}
}

El código java del ejemplo completo:

jueves, 7 de marzo de 2013

Apache POI en Apache Tapestry v.2.

Apache POI (http://poi.apache.org/) es un proyecto de Apache que nos permite trabajar, desde Java, con los documentos “office” de Microsoft. Tiene varias “sub-apis”, por un lado las que trabajan con los formatos antiguos de office (97-2007) y por otro las que trabajan con los nuevos formatos basados en xml. En este caso vamos a ver un ejemplo, muy por encima, de generación de un documento Excell utilizando el formato antiguo y cómo mostrarlo con Tapestry.

El enlace en la pantalla.
En apache tapestry una pantalla tiene dos componentes. Por un lado la parte visual (archivo .tml) y por otro lado su controlador, una clase java.  Tapestry no exige que la clase java extienda de otra o implemente tal o cual interfaz, simplemente utiliza convenios de nomenclatura. Si, por ejemplo, ponemos un enlace en nuestra página con un identificador “obtenerExcel”, lo que hará Tapestry al pulsar dicho enlace es instanciar la clase java asociada con esa pantalla (ambas relacionadas también por el nombre), ejecutar ciertos métodos de inicialización y buscar un método  llamado onActionFromObtenerExcel() para resolver la petición.

Para insertar un enlace en una pantalla utilizamos un componente de presentación de Tapestry que generará el código html necesario tanto para la presentación como para la petición que Tapestry debe recibir.  En nuestro caso queremos poner un enlace para generar un archivo Excel. El código a insertar en la pantalla será:

<t:actionlink t:id="obtenerExcel">
      <img src="${context:imagenes/excel.png}"/>
      Obtener Excel
</t:actionlink>

Cuyo resultado será:
El evento en la clase Java asociada con la pantalla.
Como he dicho antes al pulsar sobre ese enlace Tapestry ejecutará el método “onActionFromObtenerExcel()” de la clase java asociada con la pantalla.

En nuestro caso el archivo se genera al vuelo, no existirá físicamente en ninguna ubicación de nuestro servidor. Si estuviésemos enviándolo desde un servlet iríamos escribiendo sus bytes en el “response” habiéndole indicado previamente al navegador que lo que iba a recibir era un archivo Excel con la instrucción response.setContType(“application/vnd.ms-excel”).

Para que Tapestry envíe un “churro de bytes” al navegador debemos retornar un objeto del tipo StreamResponse desde nuestro método:

   StreamResponse onActionFromObtenerSabanaEnExcel() throws Exception {
         …
   }

Para indicar que el “churro de bytes” es un archivo Excel y su nombre debemos crear nuestro propio StreamResponse:

public class ExcelStreamResponse implements StreamResponse {
    private InputStream is;
    private String filename="default";
    public ExcelStreamResponse(InputStream is, String... args) {
        this.is = is;
        if (args != null) {
            this.filename = args[0];
        }
    }
    public String getContentType() {
        return "application/vnd.ms-excel";
    }
    public InputStream getStream() throws IOException {
        return is;
    }
    public void prepareResponse(Response arg0) {
        arg0.setHeader("Content-Disposition", "attachment; filename=" + filename + ".xls");
    }
}

Si nos fijamos en los métodos del StreamResponse vemos que tenemos que proporcionarle un InputStream. Esto es lógico puesto que Tapestry debe leer el “churro de bytes” para enviarlo al navegador. Esto contrasta con cualquier “parseador” que siempre necesita un outputstream sobre el que escribir. Después veremos cómo pasar ese outputstream (salida del generador del Excel) al inputstream que necesita Tapestry.

Generando el Excel con Apache POI.
Para compatibilizar el Excel generado con el office 2007 se debe utilizar el subapi
de POI, org.apache.poi.hssf, que tiene como descripción “Horrible SpreadSheet Format API's for reading/writting Excel files using pure Java”. Un par de ejemplos generados con POI son:


El primer ejemplo es muy sencillo. Primero creamos una plantilla con Excel y luego rellenamos algunos datos con POI accediendo a las filas del libro y luego a sus celdas para darles valor:

public static InputStream generarControlDeAsistencia(File plantilla, String cursoAcademico, String curso, int grupo, List<String> alumnos) throws Exception {        
        FileInputStream fis = new FileInputStream(plantilla);
        HSSFWorkbook libro = new HSSFWorkbook(fis);
        HSSFSheet hoja = libro.getSheetAt(0);
        HSSFRow fila = hoja.getRow(1);
        HSSFCell celda = fila.getCell(5);
        celda.setCellValue(cursoAcademico);
        celda = fila.getCell(8);
        celda.setCellValue(curso);
        celda = fila.getCell(11);
        celda.setCellValue(grupo);
        int totalFilas = Math.min(alumnos.size(), 36);
        for (int i=0; i<totalFilas; i++) {
            fila = hoja.getRow(7 + i);
            celda = fila.getCell(1);
            celda.setCellValue(alumnos.get(i));
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        libro.write(baos);
        baos.close();
        return new ByteArrayInputStream(baos.toByteArray());
}

El segundo ejemplo es más delicado ya que las filas y las celdas se crean dinámicamente. Para ello se debe hacer uso de métodos para crear hojas, filas y celdas y se deben definir estilos de celdas o fuentes a aplicar.

Devolviendo un InputStream. 
Como se ve al final del método generarControlDeAsistencia del apartado anterior se retorna un InputStream que necesitamos para nuestro ExcelStreamREsponse. Una vez creado éste ya lo podemos retornar como respuesta a la pulsación de enlace de la pantalla.

martes, 30 de octubre de 2012

Reestructurar el html de una tabla dinámicamente tras cambios de rowspan o colspan.

Dentro de mi editor quiero que se puedan cambiar los atributos colspan y rowspan de cualquier celda de la tabla y que esta se adapte a esos cambios. En Firefox, por ejemplo, si a una celda existente le modificas uno de estos atributos la tabla mostrara el exceso o defecto de celdas (td o th) ya que el navegador se limita a pintar lo que figura en el html y en él los tds por defecto o por exceso existen.

El primer intento ha sido intentar escribir una función que modifique el colspan de una celda directamente. El resultado ha sido una función que no me da muchas garantías en las que no tengo claro si cubro todos los casos y con limitaciones del tipo “si la celda adyacente tiene un rowspan, no se puede modificar el colspan de la celda previa”. El punto culminante ha sido al intentar duplicar esa función para tratar cambios del rowspan, con lo que podría suceder que “para modificar el rowspan la celda no debe tener un colspan”. En definitiva podría darse un caso recursivo que impidiese el realizar ciertas estructuras.  Al menos, me ha ayudado a ir viendo los distintos casos y a ver más claramente el problema.

El siguiente intento ha sido simplificar el problema a un primer paso, incrementos del colspan en una única celda y del rowspan en una única celda. Con esto se simplifican los casos y, posteriormente,  iterando se llega al mismo resultado (más costoso para el ordenador pero mucho más sencillo, más mantenible, y mucho más fiable).

Colspan + 1
1. Partimos de una celda seleccionada y queremos añadir 1 a su colspan dejando la tabla html en condiciones y teniendo en cuenta que las celdas adyacentes pueden tener muchas disposiciones posibles:
El asterisco representa la celda seleccionada que queremos ampliar con un colspan +1. Las celdas rojas son las afectadas. A la derecha de la flecha aparece el resultado final, la celda amplia su colspan, la roja desaparece o se reduce y pueden aparecer celdas nuevas (amarillas).

2. Creación de un modelo para la tabla. La mejor forma, en informática, de representar algo es mediante números. En este caso, he decidido asignar un número a cada color, o a cada celda. Con este modelo en mente, el dibujo anterior se traduce en:
Hay que tener en cuenta, también, que la celda inicial puede tener su colspan y su rowspan y que debe seguir funcionando.

3. Creamos, primero, unas funciones para representar las dos tablas, el modelo y su representación con colspans y rowspans y la página html. Las pintamos utilizando jquery. Cada celda de la tabla tiene como id su id en el modelo.

4. Dada una celda seleccionada (click) en la tabla pulsamos el botón de colspan+1. El proceso, mirando en los ejemplos es el siguiente:
  • Cogemos el id de la celda seleccionada y calculamos su cuadrado (filaIni, columnaIni, filaFin, columnaFin) utilizando para ello los valores almacenados en el modelo para ese id.
  • Recorremos la columna adyacente a la columnaFin (si no es la última, columnaFin+1) desde la filaIni hasta la filaFin.
    • Hacemos idAEliminar = modeloTabla[filaFin+1][j];
    • Si el identificador de la siguiente columna (columnaFin+2) es igual al que vamos a eliminar entonces debemos eliminar ese identificador de la columna columnaFin+1.
  • Ponemos el id de la celda seleccionada en lugar del idAEliminar.
El código de ejemplo y su funcionamiento (probado en Firefox 15) está en:

Colspan - 1
El colspan -1 es bastante más sencillo:
Simplemente se crean nuevos ids para la columna que se pierde. Si la celda tiene colspan 1 no se permite reducirlo.
El ejemplo:
Rowspan + 1 y Rowspan -1
Tal y como debería haber sido desde el principio si cambiamos col por row, i por j, y filas por columnas, tendremos los métodos para modificar el rowspan directamente desde los anteriores, sin necesidad de comernos el tarro. 
El ejemplo:
Iterando
Aquí dejo el resultado final. He modificado las funciones anteriores  para que retornen un booleano indicando si el incremento o decremento en uno se puede o no hacer en lugar de mostrar un mensaje de error. La iteración se realizará hasta que se llegue al final o se obtenga un false en una de las iteraciones. Simplemente, calculamos la diferencia entre el valor actual y el introducido para saber cuantos pasos tenemos que realizar.
El código final en:

viernes, 14 de septiembre de 2012

Propiedad toJSON de un objeto Javascript

A la hora de serializar un objeto javascript con JSON utilizando la función
JSON.stringify(value[, replacer [, space]]) 
aparte de poder utilizar el "replacer" para "customizar" (personalizar) la salida JSON de ese objeto podemos incluir la función (propiedad) toJSON en el propio objeto. El parseador la verá y hará uso de ella para crear el JSON. Esta propiedad no debe devolver el string JSON ya creado sino que deberá devolver qué propiedades del objeto se deben o no incluir en el string JSON final. Si, por ejemplo, sólo queremos que aparezca la propiedad "foo" del objeto en nuestra serialización haríamos:
     MiObjeto.prototype.toJSON = function() {
          return { foo: this.foo };
     };
Si queremos varias propiedades pero no todas, podríamos escribir:
     MiObjeto.prototype.JSON = function() {
          return { prop1: this.prop1,
                       prop2: this.prop2,
                       propQueEsArray: this.propQueEsArray };
     }

jueves, 24 de mayo de 2012

Utilizando JSON para serializar y des-serializar objetos de javascript incluyendo sus métodos (Parte 2).


Tras ojear el libro "Professional Javascript for web developers" (tercera edición), en mi opinión bastante bueno, he visto que tenía bastantes cosas que corregir en la parte 1 de este tema.
Primero, tal y como se indica allí, si codificamos nuestros objetos con el "patrón siguiente":

     function Direccion(calle, codigoPostal) {
          this.tipo = "Direccion";
         this.calle = calle,
         this.codigoPostal = codigoPostal;
         this.getCodigoPostal = function() {
              return this.codigoPostal;
         }
     }

Al crear dos instancias del objeto "Dirección" se crearán también dos instancias distintas de la función (método, objeto) getCodigoPostal. Obviamente, esto es bastante malo si queremos aprovechar memoria. Aunque en el libro se comentan otros modos para codificar nuestras "clases" parece ser que lo más correcto es:

     function Direccion(calle, codigoPostal) {
          this.tipo = "Direccion";
         this.calle = calle,
         this.codigoPostal = codigoPostal;
     }

     Direccion.prototype = {
          constructor : Direccion,
         getCodigoPostal : function() {
              return this.codigoPostal;
         }
     };

Es decir, utilizar un constructor para los atributos y luego definir los métodos extendiendo el prototipo. De este modo se consigue que cada objeto tenga sus propios atributos y que todos los del mismo tipo compartan los mismos (objetos) métodos.

En ECMASCRIPT 6 aparecerá el concepto de clase.

Si, además, consideramos que estamos ejecutando ECMASCRIPT 5 entonces ya dispondremos de JSON (sin necesidad de incluir json2.js) y del método Array.isArray(value) para comprobar si "value" es un array o no.
Con estos cambios la página queda así:
Resumiendo, si incluimos el atributo “tipo” con el nombre de la clase de nuestros objetos en cada uno de ellos, con la función siguiente podremos des-serializar nuestros objetos almecenados con JSON, conservando su tipo y sus métodos.

     function recrearObjetoDesdeJSON(objetoDesdeJSON) {
          if (!objetoDesdeJSON.tipo)
              return objetoDesdeJSON;
         var nuevoObjeto = new this[objetoDesdeJSON.tipo];
         var valorAtributo;
         for(var atributo in objetoDesdeJSON) {
              valorAtributo = objetoDesdeJSON[atributo];
              if (typeof valorAtributo == "object") {
                   if (Array.isArray(valorAtributo)) {
                       var nuevoArray = new Array();
                      for (var i=0; i<valorAtributo.length; i++) {
                           nuevoArray[i] = recrearObjetoDesdeJSON(valorAtributo[i]);
                      }
                      nuevoObjeto[atributo] = nuevoArray;
                  }
                  else
                       nuevoObjeto[atributo] = recrearObjetoDesdeJSON(valorAtributo);
}
else
                  nuevoObjeto[atributo] = valorAtributo;
}
return nuevoObjeto;
}

Debemos seguir teniendo cuidado para no referenciar un atributo desde otro:
this.atr1 = ….
this.atr2 = new Array(this.atr1, …)
Ya que el atributo del array se creara como un objeto distinto al atributo de "la clase".

miércoles, 23 de mayo de 2012

Aplicar una transformación xsl desde javascript cogiendo el archivo xml del ordenador del cliente.


Aunque podría poner una xsl  general en este ejemplo voy a utilizar la siguiente (por_un_clavo.xsl):

<?xml version="1.0"?>
     <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:template match="/">
          <xsl:apply-templates/>
     </xsl:template>
     <xsl:template match="por_un_clavo">
          <ul>
       <xsl:apply-templates/>
  </ul>
     </xsl:template>
     <xsl:template match="linea">
          <li>
       <xsl:value-of select="."/>
  </li>
     </xsl:template>
</xsl:stylesheet>

Para procesar, con ella, el xml siguiente (por_un_clavo.xml):

<?xml version="1.0" encoding="UTF-8"?>
<!-- <?xml-stylesheet type="text/xsl" href="por_un_clavo.xsl"?> -->
<por_un_clavo>
     <linea>Por un clavo se perdió una herradura,</linea>
     <linea>por una herradura, se perdió un caballo,</linea>
     <linea>por un caballo, se perdió una batalla,</linea>
     <linea>por una batalla, se perdió el Reino.</linea>
</por_un_clavo>

Si descomentamos la línea comentada podemos abrir el archivo xml con Internet Explorer y éste realizará la transformación directamente. Esto nos servirá para validar rápidamente nuestra xsl.

Si no queremos tener problemas con los acentos, las ñ y demás deberemos poner el encoding dentro del xml con el que realmente hayamos guardado nuestro archivo (trabajar con utf-8 es la mejor elección, olvidaros del ISO-8859-1 por muy latín que sea). Una forma de hacerlo, desde Windows, es escribiendo el xml en Word, guardando como texto y allí eligiendo la codificación (en Windows, por defecto y para variar, la codificación es propietaria de Microsoft y no es utf-8):


Una vez seleccionado el archivo de disco y transformado con nuestra xsl el resultado, con Firefox, será:


La página, con el código completo, se puede ver en:

El body de la página es, simplemente:

<input id="ficheroxml" type="file" name="ficheroxml" onchange="procesarXml();"/>
<div id="resultado"></div>

En él tenemos un input para seleccionar el archivo y un div que albergará el resultado de la transformación. En el momento en el que se selecciona el archivo se producirá el evento “onchange” y se llamará a la función javascript procesarXml().

Dentro del javascript hemos incluido la xsl como una variable de texto con todo su contenido:

     var xsl = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> …

y hemos creado una instancia de un lector de ficheros:

     var reader = new FileReader();

Cuando se llama a la función procesarXml se ejecuta el siguiente código:

     if (document.getElementById("ficheroxml").files.length == 0) return;
     var fichero = document.getElementById("ficheroxml").files[0];
     reader.readAsText(fichero);

donde validamos si se ha seleccionado algún archivo, cogemos el primero de ellos y le pedimos al lector que lo lea como un archivo de texto.
Si se produce algún error se lanzará el evento onerror en el lector que definimos del modo siguiente:

     reader.onerror = function(event) {
           alert("Error leyendo el archivo, code: " + reader.error.code);
     }

Si todo va bien se lanzará el evento onload sobre el reader:

      reader.onload = function(event) {
           xml = event.target.result;
   parsearXml();
      }

Aquí guardaremos el resultado de la lectura en la variable xml y llamaremos a la función que realiza el parseo e introduce el resultado en el div "resultado":

function parsearXml() {
      var xsltProcessor = new XSLTProcessor();
      var parser = new DOMParser();
      xsltProcessor.importStylesheet(parser.parseFromString(xsl, "text/xml"));
      var resultDocument = xsltProcessor.transformToFragment(parser.parseFromString(xml, "text/xml"),                  
                                       document);
      document.getElementById("resultado").appendChild(resultDocument);
}

Para que esto funcione en Chrome (18.0.1025.162 m) es necesario arrancar la instancia de  Chrome con el switch “--allow-file-access-from-files” ya que, si no, no se nos permitirá, por razones de seguridad, acceder al archivo del  disco.

martes, 22 de mayo de 2012

Hora de aventuras.

Hora de aventuras según el peque (7 añitos), con Finn, Jake, la princesa chicle, Lady Arcoiris, la princesa bultos y demás ...