Thursday 15 November 2012

Working with Google Maps API v3 and Geoserver WMS overlay of differing projection

With Google's web mapping API continuing to improve and their map display performance setting the standard, it makes sense for every GIS architect and developer to familiarise themselves with what is on offer.  Often the first problem in doing so, however, is finding a way to display your own data together with Google's base maps (well you need to make your investigation personal don't you?)

This post describes how to display WMS data from Geoserver as an overlay on top of  a Google base map using the Google Maps API v3. For added interest, the source data for the overlay is stored in the British Ordnance Survey's OSGB projection (EPSG:27700) within a SQL Server 2008 database.

I found two ways to overlay data from my Geoserver on top of a Google basemap using the Google Maps javascript API:
  • google.maps.ImageMapType for displaying OGC WMS data as tiled images.  This method may suit relatively static data sets that cover large areas. WMS layers may be tiled and cached for improved display performance using GeoWebCache.
  • google.maps.KmlLayer for displaying more dynamic (query driven) data covering smaller areas.
A note on projections
Google maps is happy receiving data in WGS84 projection (EPSG:4326) and displaying it in the Google Mercator projection (EPSG:3857, formerly 900913).  I found that life became much easier when I accepted that I needed to provide the Geoserver maps/kml in WGS84.  I also did a lot of investigation using the Proj4js library to formulate requests but produced some pretty odd looking maps (messy tile edges).  Serving the data in WGS84 greatly simplified things, bypassing the need to re-project coordinates within javascript and relying instead upon Geoserver's built in coordinate re-projection facilities as illustrated.

The javascript below is called on the page load event to populate a map div within a web page.  It creates a map centered on Lincolnshire and displays Google's roadmap as the base layer.

To add a custom overlay of census output areas from Geoserver on top of that we define a new Google ImageMapType which includes the getTileUrl function which will make one WMS request to our Geoserver for each map tile to display. This involves taking the coordinate of the top left hand corner of each map tile (as a screen coordinate) and calculating its bounding box coordinates as Latitude/Longitude. We can then build the individual WMS requests for adding the custom overlay to the map.

The code also demonstrates the use of a kmlLayer as an overlay.

//myMap.js
//javascript to create web map displaying Google base map
//with custom overlays from geoserver wms and kml

(function () {
  window.onload = function () {

    // Create a MapOptions object with the required properties for base map
    // Centered on Lincolnshire, UK
    var mapOptions = {
      zoom: 9,  
      center: new google.maps.LatLng(53.3567, -0.244), 
      mapTypeId: google.maps.MapTypeId.ROADMAP
    };

    // Create the base map 
    map = new google.maps.Map(document.getElementById('map'), mapOptions);

    //Define custom WMS layer for census output areas in WGS84
    var censusLayer =
     new google.maps.ImageMapType(
     {
       getTileUrl:
      function (coord, zoom) {
        // Compose URL for overlay tile

        var s = Math.pow(2, zoom);  
        var twidth = 256;
        var theight = 256;

        //latlng bounds of the 4 corners of the google tile
        //Note the coord passed in represents the top left hand (NW) corner of the tile.
        var gBl = map.getProjection().fromPointToLatLng(
          new google.maps.Point(coord.x * twidth / s, (coord.y + 1) * theight / s)); // bottom left / SW
        var gTr = map.getProjection().fromPointToLatLng(
          new google.maps.Point((coord.x + 1) * twidth / s, coord.y * theight / s)); // top right / NE

        // Bounding box coords for tile in WMS pre-1.3 format (x,y)
        var bbox = gBl.lng() + "," + gBl.lat() + "," + gTr.lng() + "," + gTr.lat();

        //base WMS URL
        var url = "http://mywebserver/geoserver/lincs/wms?";

        url += "&service=WMS";           //WMS service
        url += "&version=1.1.0";         //WMS version 
        url += "&request=GetMap";        //WMS operation
        url += "&layers=census_oa_2001"; //WMS layers to draw
        url += "&styles=";               //use default style
        url += "&format=image/png";      //image format
        url += "&TRANSPARENT=TRUE";      //only draw areas where we have data
        url += "&srs=EPSG:4326";         //projection WGS84
        url += "&bbox=" + bbox;          //set bounding box for tile
        url += "&width=256";             //tile size used by google
        url += "&height=256";
        //url += "&tiled=true";

        return url;                 //return WMS URL for the tile  
      }, //getTileURL

      tileSize: new google.maps.Size(256, 256),
      opacity: 0.85,
      isPng: true
     });

    // add WMS layer to map
    // google maps will end up calling the getTileURL for each tile in the map view
    map.overlayMapTypes.push(censusLayer);

    // define kml layer
    // set base WMS URL for kml request
    var kmlUrl = "http://mywebserver/geoserver/lincs/wms/reflect?";

    kmlUrl += "&version=1.1.0";              //WMS version 
    kmlUrl += "&layers=study_area_poly";     //WMS layers
    kmlUrl += "&viewparams=analysis_id:177"; //analysis_id parameter for query filter
    kmlUrl += "&styles=";
    kmlUrl += "&format=kml";

    var kmlOptions = {
      map: map,              // the map that the kml layer will be added to
      url: kmlUrl,           // the url for the kml data
      preserveViewport: true // do not adjust map location and zoom
    }; 

    // add the kml layer to the map
    studyLayer = new google.maps.KmlLayer(kmlOptions);

  };

})();

The above WMS request includes parameters to return the overlay tiles in WGS84 projection and to make areas without data to be completely transparent.  The viewparams parameter used by the KML example allows for dynamic layer creation using parameterised sql views from Geoserver.


The image illustrates the Google roads basemap with census areas (grey polygons) overlain as WMS tiles and kml data (green grid) overlain as feature data.

One of the nice features of using kml layers is that the Google Maps API automatically provides support for feature identification via info windows.

The actual web page html is provided below:

<!DOCTYPE html />
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Google Map with Geoserver WMS Overlay</title>
    <link type="text/css" href="Style/maps.css" rel="Stylesheet" media="all" />
    <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
    <script src="Scripts/myMap.js" type="text/javascript"></script>
  </head>
  <body>
    <h1>Google Map with Geoserver WMS and KML Overlay</h1>
    <div id="map"></div>
  </body>
</html>

You might question the sanity of producing web maps that require reprojection on-the-fly and I would have to agree; anything that introduces processing and delay into web mapping is to be avoided, but sometimes you just have to go with what is available.

It would be very nice to see the Google Maps API introduce support for other OGC standards such as WFS; this would enable the creation of more interactive maps and improved support for dynamic mapping of the data, especially where we are dealing with feature data that covers larger areas.

Thanks to Opinionated Geek for the html encoder, needed to make the html display correctly with Alex Gorbatchev's syntax highlighter.

6 comments:

Surendran said...

I am very much intersted on your blog.
I have tried this example, but due to some reason tt is not working

It would be great if you could provide an example link

thanks
sur

Ravi Kumar said...

Hi,

I have found a very simple way to overlay wms layer on google maps visit http://www.etechpulse.com/2013/06/how-to-add-wms-layer-as-imagepng-on.html

Anonymous said...

Very Informative Blog,
I am wondering that with geo server is not this possible to overlay the shape file as a service?
As we do it using ArcGIS server
var url = 'http://localhost:6080/arcgis/rest/services/MapServer';
var dynamap = new gmaps.ags.MapOverlay(url);
dynamap.setMap(map);

I have worked with ArcGIS Server but i am new to Geoserver.

Graham Morgan said...

Geoserver supports the use of server side shapefiles as a source (http://docs.geoserver.org/stable/en/user/gettingstarted/shapefile-quickstart/).

If you would like to incorporate client side shapefiles then your options will depend on which mapping API you are using. There are plenty of posts on using OpenLayers and js libraries to incorporate them e.g. http://indicatrix.wordpress.com/2011/12/13/shapefiles-in-openlayers/

Good Luck, Graham

Anonymous said...

Nice Blog.. It's help to all.

For more related information visit on-WMS solution.

John said...

nice write up.Calculate area of any building with Google maps.