Un nuevo post sobre visualización de datos, esta vez aplicados a la geoinformación y la visualización de mapas interactivos.
En esta ocasión usaremos Unfolding, una libreria en Java basada en Processing (del cual ya hablamos en el anterior post) y de muy facil uso, aplicada en el desarrollo de mapas interactivos y visualización de datos con información geográfica.
El ejemplo que presentamos a contianución desarrolla una sencilla aplicación de escritorio en Java (con Java Swing) que muestra un mapa interactivo donde se marcan diferentes localizaciones que representan marcas de cervezas. Al hacer click sobre un marcador cualquiera, se muestra un pop-up con información adicional del marcador.
Asi que basicamente de lo que se trata es de:
- Mostrar el mapa.
- Colocar los marcadores sobre el mapa.
- Implementar la interacción con los marcadores (mostrar el pop-up).
1. Mostrar el mapa
Como se ha mencionado con anterioridad, Unfolding está basado en Processing y usa parte de su API para realizar ciertas acciones. Para empezar usa el PApplet que ya vimos en Processing como punto de inicio de la aplicación. Además, usa el método setup() para inicializar la apliación y el método draw() para ejecutarla, así como otros métodos ya vistos para configurar e inicializar la vista principal.
Por otro lado, Unfolding ofrece diferentes proveedores de mapas a disposición del usuario. Cada uno tiene sus condiciones de uso y restricciones. En el ejemplo que se presenta, se usará OpenStreetMap, que es bastante flexible y solo limitado por la carga de uso, sin muchas más restricciones. Una vez inicializado el mapa, establecemos una posición de origen mediante sus coordenadas geográficas de latitud y longitud.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/** * Birra applet. * * @author lagarcia */ public class BirraApplet extends PApplet { ... /** * @see processing.core.PApplet#setup() */ @Override public void setup() { /* Set canvas size */ size(800, 600); /* Set proxy settings, if needed */ Properties systemSettings = System.getProperties(); systemSettings.put("http.proxyHost", "proxy.YYYYY.es"); systemSettings.put("http.proxyPort", "8080"); systemSettings.put("http.proxyUser", "lagarcia"); systemSettings.put("http.proxyPassword", "XXXXXXX"); System.setProperties(systemSettings); /* Init map */ birraMap = new UnfoldingMap(this, new OpenStreetMap.OpenStreetMapProvider()); /* To basically interact with the map */ MapUtils.createDefaultEventDispatcher(this, birraMap); /* Set origin */ Location madridLocation = new Location(40.4f, -3.7f); birraMap.zoomAndPanTo(madridLocation, 6); /* Set restrictions */ birraMap.setZoomRange(6, 8); } /** * @see processing.core.PApplet#draw() */ @Override public void draw() { /* Draw map */ birraMap.draw(); } ... } |
El resultado sería este:
2. Colocar los marcadores sobre el mapa
Una vez tenemos el mapa, podemos proceder a crear los marcadores con la información pertinente asociada a cada uno y situarlos sobre el mapa.
Para ello lo que vamos a hacer es cargar cierta información de un fichero. Esa información contiene la localización geográfica en la que se colocará el marcador (longitud y latitud), el nombre del marcador y los datos asociados al marcador que se mostrarán posteriormente mediante un pop-up interactivo. Estos son los datos:
# Cervezas artesanas de España |
# Ciudad, latitud, longitud, cerveza1-cerveza2-cerveza3-cerveza4 |
Salamanca,41.0,-5.6,Malasombra-Lega-Bizarra-Helmantica |
Madrid,40.4,-3.7,La Virgen-Cibeles-Maravillas |
Asturias,43.32,-5.9,Caleya |
Vamos por pasos:
- Cargamos los datos en una lista interna de DTOs (beans):
1 2 3 |
/* Load data */ DataLoader dl = new DataLoader(); dl.loadData("data/birras.csv"); |
- Crear los marcadores:
1 2 |
/* Get markers */ List<BirraMarker> markers = dl.getBirraMarkers(loadImage("img/beer-icon.png")); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * Get the markers list. * * @param imgMarker * @return */ public List<BirraMarker> getBirraMarkers(PImage imgMarker) { List<BirraMarker> birraMarkers = new ArrayList<BirraMarker>(); for (BirraTown bt : birras) { BirraMarker marker = new BirraMarker(bt, imgMarker); birraMarkers.add(marker); } return birraMarkers; } |
La clase BirraMarker se extiende de SimplePointMarker y tiene unos atributos específicos que el mapa usará para situar y dibujar el marcador sobre él, como el icono (PImage img). También tiene un método draw() que se invoca para dibujar el marcador. Ahí se puede indicar la posición en la que se quiere dibujar la imagen asociada al marcador. Por ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * @see de.fhpotsdam.unfolding.marker.AbstractMarker#draw(processing.core.PGraphics, float, float) */ @Override public void draw(PGraphics pg, float x, float y) { pg.pushStyle(); pg.pushMatrix(); pg.imageMode(PConstants.CORNER); // The image is drawn in object coordinates, i.e. the marker's origin (0,0) is at its geo-location. pg.image(img, x - (img.width / 2), y - (img.width / 2)); pg.popMatrix(); pg.popStyle(); } |
- Añadimos los marcadores al mapa:
1 2 3 4 |
/* Add markers to map */ for (Marker m : markers) { birraMap.addMarkers(m); } |
Como se ve en el ejemplo, se usan múltiples clases y APIs de Processing, como PImage, loadImage(), etc.
3. Mapa interactivo
Por último, haremos el mapa interactivo. La intención es que al hacer click con el ratón sobre cada marcador, se muestre un pop-up sobre él con información adicional. Al volver a hacer click, o hacer click en otro marcador, el pop-up desaparece. Además también queremos mostrar otro pop-up sobre cada marcador si se coloca el puntero sobre él, que desaparezca cuando éste se vaya. Aquí mostraremos el nombre del marcador.
Ámbos eventos podemos definirlos mediante la API del PApplet heredada de Processing. Este serái el método para el evento de click sobre el mapa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * @see processing.core.PApplet#mouseClicked() */ @Override public void mouseClicked() { if (markers == null) { return; } /* Clear selection */ for (Marker m : markers) { m.setSelected(false); } /* Select clicked marker */ Marker m = birraMap.getFirstHitMarker(mouseX, mouseY); if (m != null) { m.setSelected(true); } } |
Si no hay marcadores, devolvemos nulo, es decir, no hacemos nada. Si hay marcadores, primero deseleccionamos todos, ya que no admitimos selección múltiple en nuestro ejemplo. Despues detectamos cual es el primer marcador seleccionado en la posición del puntero (Marker m = birraMap.getFirstHitMarker(mouseX, mouseY)) y lo marcamos como tal (m.setSelected(true)).
El método para definir el evento de movimiento del ratón sería el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * @see processing.core.PApplet#mouseMoved() */ @Override public void mouseMoved() { if (markers == null) { return; } /* Clear selection */ for (Marker m : markers) { ((BirraMarker) m).setHover(false); } Marker m = birraMap.getFirstHitMarker(mouseX, mouseY); if (m != null) { ((BirraMarker) m).setHover(true); } } |
El procedimiento es practicamente el mismo que en el caso anterior. Tan solo en lugar de establecer el marcador como seleccionado, se usa setHover(true).
Tan solo nos falta añadir al método draw() del marcador el código que permitirá dibujar los pop-ups en caso de que éste sea seleccionado o sobrevolado por el puntero. El método definitivo quedaría así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
/** * @see de.fhpotsdam.unfolding.marker.AbstractMarker#draw(processing.core.PGraphics, float, float) */ @Override public void draw(PGraphics pg, float x, float y) { pg.pushStyle(); pg.pushMatrix(); pg.imageMode(PConstants.CORNER); // The image is drawn in object coordinates, i.e. the marker's origin (0,0) is at its geo-location. pg.image(img, x - (img.width / 2), y - (img.width / 2)); /* Selected */ if (selected) { /* Rounded rectangle */ pg.translate(x, (int) (y - (this.numBirras * 12.5))); pg.stroke(0); pg.fill(255); pg.strokeWeight(1); pg.rectMode(PConstants.CENTER); pg.rect(0, 0, elipseDim.width, elipseDim.height, 7, 7); /* Triangle */ pg.beginShape(); pg.vertex(-5, (int) (this.numBirras * 8.75)); pg.vertex(5, (int) (this.numBirras * 8.75)); pg.vertex(0, (int) (this.numBirras * 12.5)); pg.endShape(PConstants.CLOSE); /* Text */ pg.fill(0); pg.text(this.text, -((elipseDim.width / 2) - 5), -((elipseDim.height / 2) - 12)); } /* Hover */ else if (hover) { pg.translate(x, y); pg.stroke(0); pg.fill(255); pg.strokeWeight(1); pg.rectMode(PConstants.CENTER); pg.rect(40, 0, 80, 20, 7, 7); pg.fill(0); pg.textAlign(PConstants.CENTER); pg.text(this.name, 40, 5); } pg.popMatrix(); pg.popStyle(); } |
Como se ve en el método, se usan los atributos selected y hover que se marcan en los métodos anteriores de eventos para determinar el estado del marcador. Hay que recordar que el método draw() se está invocando de forma contínua tal y como se usa en Processing.
Este sería el resultado final:
Podeis descargar el proyecto completo desde aquí. !Ahora a darle a los mapas!