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.