Thursday, May 24, 2012

ARCGIS JavaScript API –Using Google Streetview with WMS


1.     Introduction


Before writing section 2 of editing, I first did a small experiment of integrating non ESRI GIS data into a  HTML5 application based on the ArcGis JavaScript API, mainly to show how easy this can be done in a structured way.
As an example I will show the usage of the popular Google Streetview in combination with a WMS service that contains addresses from a region in Belgium. The following steps will be walked through :

·         Load WMS service that contains the address contents. When you look in ArcGis Online, a lot of maps are available for the Flanders region. One I was interested is the ‘GDI-Flanders - INSPIRE View Service – Addresses’ map. This is maintained by AGIV, a governmental organization. I am not sure the contents will stay for free, but for now I can use it to implement WMS support.

·         Because I need a spatial query on the WMS layer, I had to implement a query task for WMS layers.

·         Based on the result of the address selected by the spatial query on the address layer, we implement the Google Streetview based on the Google Streetview API.

What we see here is that there are layers coming from different sources, resulting in different spatial references. Lucky, ESRI helps us to transform geometries from one spatial reference to another through geometry services, that will be covered to in this document.

The final result looks like this, based on my home address :

With the WMS address layer from AGIV


2.     WMS Service


In my initial version of the ArcGis application framework there was no support build in for WMS services. In the ArcGis JavaScript API I found only limited support build in for WMS services.

Creation of a WMS service  to  be added to the map can be done in two ways. You can use either only the URL as parameter, or you can use a layer info object with the URL. In case you only use the URL, the ESRI API will use the proxy defined in the application, as is  required for doing editing. I first tried this approach because it requires little parameters. However I got an error when using a local proxy defined as explained in an earlier document. So I moved to the second approach, specifying a layer info data in the XML configuration as showed below.

<Layer title="AGIV adrespunten" serviceType="Wms" visibleInitial="true" expandable="false" showLabel="false" restURL="http://wms.agiv.be/inspire/wms/adressen" icon="../Images/icons/i_streets.png">
        <WmsExtent  xmin="2" ymin="50" xmax="6" ymax="52" spatialReference="4258"></WmsExtent>
        <WmsLayerInfos>
                <LayerInfo name="Adrespos" title = "Address points"> </LayerInfo>
        </WmsLayerInfos>
</Layer>

Implementing this in the configuration component results in only adding a couple of lines in the REST service. The only point of importance is the retrieval of the information needed to fill in the XML configuration for the WMS service. There are a couple of ways to retrieve the needed information. A first procedure you can use is using the “GetCapabilities” method of the rest service. The drawback of this procedure is that you get a large XML with a mess of properties. A second method is the use of a free utility “Gaia”. This utility is very easy to get information like the name or the spatial reference of a WMS layer. At the same time you can also visualize the WMS layers.

Another issue I encountered was during the initializing of the WMS layer with the ArcGis JavaScript API. In my GIS framework the building of the tables was done in the onload event of the layers. However in the case of WMS layers, no onload event is triggered. I should expect that as with other layers, the full data of the layer is available after the onload. A solution could be by doing a GetCapabilities on the WMS service to retrieve more information. This approach was used in the build of the list of selectable layers to get more information of the WMS service.


function _addNewWmsLayer(baseLayer, serviceType, index) {
   try {
            var layerInfos = new Array();
            var layerNames = new Array();
            for (var i = 0; i < baseLayer.LayerInfos.length; i++) {
              var layer = new esri.layers.WMSLayerInfo({ name: baseLayer.LayerInfos[i].Name, title: baseLayer.LayerInfos[i].Title });
              layerInfos.push(layer);
              layerNames.push(baseLayer.LayerInfos[i].Name);
            }
            var resourceInfo = {
                extent: new esri.geometry.Extent(baseLayer.WmsExtent.xmin, baseLayer.WmsExtent.ymin, baseLayer.WmsExtent.xmax, baseLayer.WmsExtent.ymax,
                    { wkid: baseLayer.WmsExtent.spatialReference }),
                layerInfos: layerInfos
            };
            mapLayer = new esri.layers.WMSLayer(baseLayer.RESTURL, { resourceInfo: resourceInfo, visibleLayers: layerNames });
            mapLayer.setImageFormat("png");
            mapLayers.push(mapLayer);
            map.addLayer(mapLayer);
            baseMapLayerInfos.push(new baseMapLayerInfo(baseLayer.RESTURL, mapLayer.id, baseLayer.Title, baseLayer.LayerInfos[0].Name, serviceType));
            verifyInitialisationMap();
        } catch (e) {
            throw new Error("wmslayer " + baseLayer.RESTURL + " initialize process failed. " + e.name + "\n" + e.message);
        }
    }

The only extra information that you can get at this stage is the id of the layer, only after the WMS layer is added to the map. This is wy I only save the layer information after it has been added to the map. Other information as name, spatial reference comes from the configuration.
As you can see, not very exciting for adding  WMS layer information to our map. I did need to take some additional steps in order to have the possibility to make spatial querying the WMS service possible.

if (layerLegendInfo.serviceType == ArcGISServiceType.Wms) {
// Wms file
  urlGet = layerLegendInfo.url;
  viewModel.wmsUrl = layerLegendInfo.url;
  viewModel.wmsSpatialReference = GisOperation.getMap().getLayer(layerLegendInfo.id).spatialReference;
  var data = {
     VERSION: '1.1.1',
     REQUEST: 'GetCapabilities',
     SERVICE: 'WMS'
  };
  $.ajax({
      type: 'GET',
      url: urlGet,
      data: data,
      dataType: "xml",
      success: function (xml) {
          var jsonContents = $.xml2json(xml);
           viewModel.wmsName = jsonContents.Capability.Layer.Layer.Name;
          if (jsonContents.Capability.Layer.Layer.queryable == '1') {
          // Layer is querable and can be used for selection
            for (var j = 0; j < GisOperation.getBaseMapLayerInfos().length; j++) {
               if (GisOperation.getBaseMapLayerInfos()[j].baseMapLayerId == viewModel.wmsName && GisOperation.getBaseMapLayerInfos()[j].serviceType == 5) {
                 selectionInfo = new layerSelectionInfo(GisOperation.getBaseMapLayerInfos()[j].url, GisOperation.getBaseMapLayerInfos()[j].id, GisOperation.getBaseMapLayerInfos()[j].name, null, "", GisOperation.getLayersSelectionInfos().length, null, ArcGISServiceType.Wms);
                 GisOperation.getLayersSelectionInfos().push(selectionInfo);
               }
            }
          }
          terminateLayerInitialize();
      },
      error: function (jqXHR, textStatus, errorThrown) {
          logMessage("E", "error in selectBuild AJAX call ->" + textStatus + "\n" + errorThrown, "layerSelectionModule");
     }
  })
}

The main thing here is that we use the getCapabilities method to extract some additional information about the WMS layer. The most important here is that we use the querable attribute to know if we can query this WMS layer.

3.     WMS query task


Once we added the WMS layer into our map, we can start creating a WMS query task. If you look at the WMS documentation, it looks simple. However there are some problems around the corner. When you click at a location in the map, you will get coordinates based on the spatial reference from the map, and this is based on the first base map layer loaded. But as you saw in the WMS layer definition, the WMS expect us to return the coordinates based on the spatial reference of the WMS layer. So there is the need for doing a projection conversion. Using the ESRI API you will need to call the projection conversion method of a geometry service running on an ArcGis server.

So before we can start implementing a WMS query task, we will first need to add a projection conversion method, I will use therefor the GisGeoProcessing class. It is not an exciting method, but must be done asynchronous.

convertProjection: function (points, toSpacialReference, callback) {
   GisOperation.getGeometryService().project(points, toSpacialReference,
      function (projectedPoints) {
           callback(projectedPoints);
      }, function errorprojected(err) {
            logMessage("E", "projection convertion failed -->" + err.message, "GisGeoProcessing");
            callBack(null);
  });
}

The geometry service has already been initialized during the startup of the application and is available in the GIS framework.
I created a new GIS class ‘GisExtra’ that contains the new methods needed to illustrate the use of WMS queries and Google Streetview. The implementation of the WMS query is mainly the use of a REST service that will return the features from the spatial query.

wmsQuery: function (url, layerName, screenPoint, callBack) {
  var extent = gisOperation.getMap().extent;
  var points = new Array();
  points.push(new esri.geometry.Point(extent.xmin, extent.ymin, extent.spatialReference));
  points.push(new esri.geometry.Point(extent.xmax, extent.ymax, extent.spatialReference));
  GisGeoProcessing.convertProjection(points, viewModel.wmsSpatialReference,
      function (convertedPoints) {
          var extent = new esri.geometry.Extent(convertedPoints[0].x, convertedPoints[0].y, convertedPoints[1].x, convertedPoints[1].y, convertedPoints[0].spatialReference);
          var urlGet = GisOperation.getLayersLegendInfos()[i].url;
          var data = {
               REQUEST: 'GetFeatureInfo',
               SERVICE: 'WMS',
               VERSION: '1.1.1',
               LAYERS: layerName,
               STYLES: '',
               FORMAT: 'image/png',
               BGCOLOR: '0xFFFFFF',
               TRANSPARENT: 'TRUE',
               SRS: 'EPSG:' + extent.spatialReference.wkid.toString(),
               BBOX: extent.xmin.toString() + "," + extent.ymin.toString() + "," + extent.xmax.toString() + "," + extent.ymax.toString(),
               WIDTH: gisOperation.getMap().width.toString(),
               HEIGHT: gisOperation.getMap().height.toString(),
               QUERY_LAYERS: layerName,
               X: screenPoint.x.toString(),
               Y: screenPoint.y.toString()
         };
         $.ajax({
              type: 'GET',
              url: urlGet,
              data: data,
              dataType: "xml",
              success: function (xml) {
                   var jsonContents = $.xml2json(xml);
                     callBack(jsonContents);
                   },
              error: function (jqXHR, textStatus, errorThrown) {
                     logMessage("E", "error in GetFeatureInfo AJAX call ->" + textStatus + "\n" + errorThrown, "GisExtra/wmsQuery");
             }
         });
    });
},

We first start an asynchronous operation for converting the extent coordinate of the map into coordinates that corresponds with the spatial reference of the WMS service. The other information is related to screen information. Apparently, WMS service do a calculation base on the map screen coordinates and screen point coordinates together with the map extent expressed in geo coordinates.
As you can see, simple querying a WMS services results in three asynchronous actions. The purpose of a good framework is hiding this complexity from the interface.

4.     Google Streetview API


After I have written the WMS query task, I am ready to implement the visualization of Streetview for an address selected. To simplify the GUI, I added an extra tab to the on the web page. I called it “Extra”. With the help of DOJO, I will add dynamically the Google output on the page.

With the help from knockoutJS , the command is initiated and I start the focus of the tab control on the tab ‘Extra’. Then we initiate a draw point operation.

streetviewCommand: function () {
     try {
        viewModel.tabIndex(3);
        // Build the contents
        var tabDigit = dijit.byId(tabNames[3]);
        if (dojo.byId("titlestreetviewId") == null) {
           var contentTitle = "<div id='titlestreetviewId'><pan class='extraTitle'>Streetview information, select address</pan></div>";
           require(["dojo/dom-construct"], function (domConstruct) {
                 domConstruct.place(contentTitle, "extraContents", "first")
           });
        }
        // Start gis operations
        GisOperation.getSelectLayer().clear();
        GisOperation.getMeasurement().clearResult();
        GisCommonTasks.setFinishedInfoEvent(handleAddressResults);
        GisCommonTasks.drawGeometry("point")
    } catch (e) {
       logMessage("E", "streetviewCommand", e.Message);
    }
}

The callback of the draw action will bring us to the next step, doing a spatial query on the WMS layer to get detailed information from the address layer.

function handleAddressResults(geometry) {
   try {
     GisCommonTasks.setFinishedInfoEvent(null);
     if (geometry != null) {
         if (geometry.type == "point") {
            // Execute spatial query
            wmsPoint = geometry;
            var screenPoint = GisOperation.getMap().toScreen(geometry);
            GisExtra.wmsQuery(viewModel.wmsUrl, viewModel.wmsName, screenPoint, wmsQueryComplete);
         }
     }
   } catch (e) {
     logMessage("E", "error in handleAddressResults->" + e.name + "\n" + e.message, "commonToolbarVM");
   }
}

Not much coding needed for the spatial query on the WMS layer. Again the callback method will guide us to the final step, executing the Google API to get the Streetview on the web page. Because Google Maps use WGS-84, we need another projection conversion before we can get to the result. So again an asynchronous operation will be needed. To separate the business logic for the Streetview functionality, I put the method into the newly created GisExtra class.

First I put some extra data coming from the WMS layer on the tab. This is not the correct way in the MVVM model, but I could find another solution, certainly with the Google Streetview coming.

Next we convert the point geometry into the WGS-84 equivalent.
Then we called the Google API.

function wmsQueryComplete(featureInfo) {
   try {
     if (featureInfo.FIELDS) {
         // Add contents to the window
         $("#wmsInfoId").remove();
         $("#streetviewId").remove();
         var contentPosition = "<div id='wmsInfoId'><span>NisCode:" + featureInfo.FIELDS.NISCODE + " CapaKey:" +featureInfo.FIELDS.CAPAKEY + "</span></div>";
         require(["dojo/dom-construct"], function (domConstruct) {
            domConstruct.place(contentPosition, "extraContents", "last")
         });
         var infoTemplate = new esri.InfoTemplate("Agiv Location", "");
         var wmsSymbol = new esri.symbol.SimpleMarkerSymbol().setStyle(esri.symbol.SimpleMarkerSymbol.STYLE_SQUARE).setColor(new dojo.Color([255, 0, 0, 0.5]));
         graphic = new esri.Graphic(wmsPoint, wmsSymbol, {}, infoTemplate);
         GisOperation.getSelectLayer().add(graphic);
     }
     // Interface to google streetview use 4326 (wgs 84)
     GisGeoProcessing.convertProjection([wmsPoint], new esri.SpatialReference({ wkid: 4326 }), function (point) {
         if (point[0] != null) {
          //                    clearTabContents(3);
             if (dojo.byId("streetviewId") == null) {
               var contentPosition = "<div id='streetviewId' style='width: 360px; height: 360px'></div>";
               require(["dojo/dom-construct"], function (domConstruct) {
                  domConstruct.place(contentPosition, "extraContents", "last")
               });
             }
             GisExtra.initStreetView(point[0], dojo.byId("streetviewId"), 50);
          }
     });
 } catch (e) {
    logMessage("E", "wmsQueryComplete", e.Message
 }
}

Again a nice chaining of callback’s guide us to the ultimate action to be done, calling the Google API. As you can see, not a lot of parameters are needed. We need a point in  WGS-84, a div that will contain the image and a radius that will help us solve the facts that the address points need not to be on the street itself but may have an offset to the street, as is the case in the AGIV address locations.
The method creating the image is simple and examples can be found on the Google documentation.
        initStreetView: function (point, divCtrl, radius) { 
            try {
                pointStreetView = point;
                locationGoogle = new google.maps.LatLng(point.y, point.x);
                // Set up the map and enable the Street View control.
                var mapOptions = {
                    center: locationGoogle,
                    zoom: 16,
                    mapTypeId: google.maps.MapTypeId.ROADMAP
                };
                var mapGoogle = new google.maps.Map(divCtrl, mapOptions);
                var panorama = mapGoogle.getStreetView();
                // Set up Street View and initially set it visible. Register the
                var panoOptions = {
                    position: locationGoogle,
                    visible: true
                };
                panorama.setOptions(panoOptions);
                // Create a StreetViewService object.
                var streetviewService = new google.maps.StreetViewService();
                // Compute the nearest panorama to the Google Sydney office
                // using the service and store that pano ID
                streetviewService.getPanoramaByLocation(locationGoogle, radius,
                    function (result, status) {
                        if (status == google.maps.StreetViewStatus.OK) {
                            // We'll monitor the links_changed event to check if the current
                            // pano is either a custom pano or our entry pano.
                            google.maps.event.addListener(panorama, 'links_changed',
                              function () {
                              });
                        }
                    });
            } catch (e) {
                    logMessage("E", "error in Google streetview API interface ->" + err.message, "GisExtra/initStreetView");
            }
        }

The great thing here is that you can create almost the same streetview functionality as that Google Maps expose, maybe not at the same speed. You can explore the street. I wonder if it is not possible to synchronize the Google window with the ArcGis Map. What I show here has certain usage restriction, keeps this in mind.

The biggest job was supporting WMS layers and query functionality. This tokes the most of my time. But after all not so much code is required to support Google Streetview and WMS services. You can still add a lot more functionality to WMS services, but who wonders, maybe one day they will be added to the ArcGis JavaScript API.

Walking into my street from a ArcGis JavaScript Application looks like.