Integración de un servicio WMS en GoogleMaps usando GWT

Este artículo es una continuación del artículo “Google Web Toolkit & Google Maps” que se publicó en este mismo blog hace un par de meses. La idea de escribirlo surgió debido a una pregunta que nos hizo una lectora en la que preguntaba cómo se podía integrar un servidor WMS propio en Google Maps utilizando GWT.

Este artículo explica paso a paso cómo hacerlo y para ello se ha tomado como servidor de ejemplo un WMS de la NASA, pero se ha escrito el código de forma que haciendo unas pocas modificaciones se puede integrar cualquier otro servidor WMS.

wms

Se parte de un entorno en el que se ha creado un proyecto de GWT llamado GoogleMaps-WMS utilizando para ello el entorno de desarrollo Eclipse. Además se ha creado una página de inicio y se ha añadido un mapa de GoogleMaps en el centro. Cómo crear el proyecto y cómo crear la página de inicio es algo que ya se explicó en el artículo anterior.

Antes de empezar a ver código es necesario entender cómo funciona el sistema de tileado de GoogleMaps basado en un QuadTree, que no es más que una estructura de datos muy utilizada para crear índices espaciales. El QuadTree  se aplica sobre un área concreta y en el caso de GoogleMaps se aplica sobre toda la superficie terrestre que varía entre la longitud  -180º a 180º y la latitud -90º a 90º (estas cifras no son del todo exactas, ya que la latitud se “acorta” unos grados en los polos)

El QuadTree divide  esta superficie en 4 áreas en forma rectangular de tamaño fijo, que en su conjunto pueden ser vistas como una matriz de 2×2 y se les pueden dar nombres según la fila y la columna en la que están. De aquí en adelante  a esas áreas rectangulares las vamos a llamar tiles. La nomenclatura de Google es numérica y crece de Oeste a Este y de Norte a sur de forma que el tile de la parte superior izquierda es el (0,0), el tile de la parte superior derecha es el (1,0), el de la parte inferior izquierda es el (0,1) y el de la parte inferior derecha es el (1,1). Esta matriz de 2×2 es la matriz de tiles para un nivel de zoom 1.

Cada una de esos tiles se puede descomponer a su vez en 4 partes iguales de área constante. Si dividimos los 4 tiles que hay inicialmente en 4 partes iguales obtendremos una matriz de 4×4 tiles, que nombraremos matriz de tiles de nivel 2. Podemos utilizar la misma nomenclatura que hemos utilizado para los tiles de nivel de zoom 1, siendo el tile de la esquina superior izquierda el (0,0) y el tile de la esquina inferior derecha el (3,3).

Este proceso de descomposición de tiles en 4 tiles de un nivel inferior lo podemos realizar N veces de forma que al final tendremos una estructura de datos de N niveles en los que en cada nivel habrá 4 veces más tiles que en el nivel predecesor. En el caso de GoogleMaps cada uno de estos tiles es una imagen de 256×256 píxels independientemente del nivel en el que nos encontremos, por lo que a mayor nivel de zoom más detalle tendrá la imagen. Para poder entender mejor la nomenclatura de los tiles de Google podemos consultar ésta página web.

Una vez entendido el sistema de tileado y la nomenclatura que usa GoogleMaps, vamos a empezar a escribir código.  Existe en el API  que proporciona Google una clase llamada TileLayer que representa un proveedor de tiles del que se nutrirá el mapa para poder visualizarse. Tenemos por tanto que crear una clase llamada WMSTileLayer que herede de TileLayer, lo que implica que hay que implementar algunos métodos:

public class WMSTileLayer extends TileLayer{
public WMSTileLayer(String baseServerUrl, String layers, String styles, Projection projection){
super();
}
@Override
public String getTileURL(Point tile, int zoom) {
return null;
}
@Override
public double getOpacity() {
return 1.0;
}
@Override
public boolean isPng() {
return true;
}

En el método getOpacity hay que devolver la transparencia de la imagen que varía entre 0 y 1. Si queremos que la imagen se visualice en el mapa tendremos que poner el valor 1.0.

En el método isPng hay que indicar si la imagen que se devuelve es png. En nuestro caso hemos devuelto true ya que las imágenes devueltas las pedimos en png.

Pero en el tercer método de la clase es dónde está la complejidad de esta clase. El método getTileURL acepta como parámetros un objeto de tipo Point y un entero. El entero hace referencia el nivel de zoom mientras que el Point es una pareja de enteros que hacen referencia al nombre del tile que hay que devolver.  Si hemos entendido la estructura de QuadrTree y la nomenclatura que usa Google en los tiles podremos entender el significado de estos parámetros. A medida que el usuario se desplaza por el mapa, el componente encargado de recuperar los tiles invoca al método getTileURL para recuperar la URL de los tiles del área en la que se encuentra. Este método se invocará normalmente unas cuántas veces por cada vez que el usuario modifique el área del mapa que está visualizando.

En el ejemplo de código hemos visto cómo en la implementación del método getTileURL se devolvía el valor null y ahora lo vamos a modificar para que devuelva la URL del tile que se está pidiendo en los argumentos del método y que  tendrá que ser descargado desde un servidor WMS. Podríamos haber creado un componente específico para un servidor WMS concreto, pero en lugar de eso hemos implementado un componente algo más genérico para que se pueda utilizar con otros servidores de mapas.

Para ello hemos añadido a la clase WMSTileLayer algunos parámetros que se establecen en el constructor. Estos parámetros son la dirección del servidor WMS, la lista de capas a cargar y los estilos con los que se desea visualizar la capa. Cómo conocer los valores de la capa(s) que soporta un servidor o los valores del estilo(s) soportados está fuera del alcance de este artículo, pero bastaría con hacer la operación GetCapabilities sobre el servidor.

El código resultante quedaría del siguiente modo:

public class WMSTileLayer extends TileLayer{
private String baseServerUrl;
private String layers;
private String styles;
private Projection projection;
private String getMapUrl = null;
public WMSTileLayer(String baseServerUrl, String layers, String styles, Projection projection){
super(null, 0, 20);
this.baseServerUrl = baseServerUrl;
this.layers = layers;
this.styles = styles;
//construct the base URL
String urlBase = baseServerUrl + "?" +
"FORMAT=image/png&SERVICE=WMS&REQUEST=GetMap&WIDTH" +
"=256&HEIGHT=256&VERSION=1.1.0&" +
"STYLES=" + styles + "&" +
"LAYERS="+ layers + "&";
getMapUrl = urlBase;
}
@Override
public String getTileURL(Point tile, int zoom) {
return null;
}
@Override
public double getOpacity() {
return 1.0;
}
@Override
public boolean isPng() {
return true;
}

Además de los parámetros comentados anteriormente, podemos ver que se ha incluido un cuarto parámetro llamado projection, que se utiliza para identificar la proyección y deberá ser el mismo que el utilizado para crear el componente contenedor de mapas de GoogleMaps.

En el constructor lo que se está haciendo es básicamente crear la URL que servirá de base para lanzar la petición GetMap a un servidor WMS.Todavía faltan algunos parámetros para que la petición sea completa, pero estos parámetros los vamos a añadir en el método getTileURL que es el último método que nos queda por ver para terminar con la explicación.

Para simplificar el problema, vamos a suponer que el servidor WMS que estamos utilizando soporta el sistema de coordenadas en latitud/longitud (EPSG:4326). El problema será entonces pasar un tile de GoogleMaps definido mediante un nombre y un nivel de zoom a un par de coordenadas que definan el mismo tile en latitud/longitud. Para ello utilizamos el API de GoogleMaps y una vez tengamos ese tile, creamos la Url que lo contiene, de modo que el método getTileURL quedará de la siguiente forma:

@Override
public String getTileURL(Point tile, int zoom) {
//Transforma las coordenadas a coordenadas Lat/Lon
Point tileIndexLLPoint = Point.newInstance(tile.getX() * 256, (tile.getY() + 1) * 256);
Point tileIndexURPoint = Point.newInstance(((tile.getX() + 1) * 256), (tile.getY()) * 256);
LatLng llPoint = projection.fromPixelToLatLng(tileIndexLLPoint, zoom, true);
LatLng urPoint = projection.fromPixelToLatLng(tileIndexURPoint, zoom, true);
String url =  getMapUrl + "SRS=EPSG:4326&BBOX=" +
	llPoint.getLongitude() + "," +
	llPoint.getLatitude() + "," +
	urPoint.getLongitude() + "," +
	urPoint.getLatitude();
return url;
}

En las variables llPoint y urPoint se calculan las coordenadas de la esquina inferior izquierda y de la esquina superior derecha del tile que en latitud/longitud equivale al tile de GoogleMaps. Añadiendo estas coordenadas a la URL mediante el parámetro BBOX y añadiendo el parámetro SRS se forma la URL que se devuelve en el método y que será usada por el componente gráfico para recuperar las imágenes y mostrarlas en pantalla. Ahora ya sólo falta crear un objeto WMSTileLayer y asociarlo al componente gráfico de GoogleMaps. La clase que hace esto es GoogleMaps_WMS, y el código es el sigueinte:

public class GoogleMaps_WMS implements EntryPoint {
 private MapWidget map;
 private String wmsURL = "http://wms.jpl.nasa.gov/wms.cgi";
 private String wmsLayers = "global_mosaic_base";
 private String wmsStyles = "pseudo";
 private String mapName = "Nasa";
 public void onModuleLoad() {
//Coordenadas centrales para posicionar el mapa
LatLng latLonCenter = LatLng.newInstance(0,0);
//Creamos el mapa con un tamaño fijo
map = new MapWidget(latLonCenter, 2);
map.setSize("520px", "520px");
Projection projection = new MercatorProjection(20);
//Creamos el servicio WMS que nutrirá a GoogleMaps
MapType myWMSMap = new MapType(new TileLayer[] {
new WMSTileLayer(wmsURL, wmsLayers, wmsStyles, projection)},
projection,
mapName);
//Añadimos el servicio WMS al mapa
map.addMapType(myWMSMap);
//Añadimos algunas opciones al mapa
MapUIOptions options = MapUIOptions.newInstance(Size.newInstance(400,400));
options.setMapTypeControl(true);
map.setUI(options);
// Add the map to the HTML host page
RootPanel.get("mapContainer").add(map);
}
}

En el ejemplo, la clase tiene algunos atributos:

  • wmsURL: para definir la URL del servicio WMS.
  • wmsLayers: lista separada por comas de las capas a mostrar.
  • wmsStyle: estilo con el que se desea mostrar la capa
  • mapName: nombre del mapa que aparecerá en el interfaz de usuario de GoogleMaps

Cambiando estos atributos se puede acceder a cualquier otro servidor WMS.

VN:F [1.7.7_1013]
Rating: 10.0/10 (3 votes cast)
Integración de un servicio WMS en GoogleMaps usando GWT10.0103
  • Share/Bookmark
    • Montse
    • January 18th, 2010

    Y como se podría en lugar de trabajar con WMS integrar una tilecache?

    UN:F [1.7.7_1013]
    Rating: 0.0/5 (0 votes cast)
      • jpiera
      • January 18th, 2010

      Si las tiles son las mismas que las tiles utilizadas en Google Maps es un problema de convertir la nomenclatura de tileado de Google Maps en la nomenclatura de tu servicio de tiles para saber qué tile tienes que devolver.

      Si la estructura de tiles es diferente no tengo ni idea de cómo se podría hacer, ya que tendrías que unir y/o recortar imágenes.

      UA:F [1.7.7_1013]
      Rating: 0.0/5 (0 votes cast)
    • vegatripy
    • January 18th, 2010

    Hmmm,. buen artículo pero… ¿y si queremos integrar un servidor WMS con la API de Google Maps V3 sin usar el GWT?¿
    Casi todos los ejemplos que he encontrado en la web están basados en la Version2 y me tienen perdido :S

    UN:F [1.7.7_1013]
    Rating: 0.0/5 (0 votes cast)
  1. January 18th, 2010