VEGeocodingService with Silverlight MapControl CTP


VEGeocodeService Silverlight MapControl
Fig 1 – VEGeocodeService and Silverlight MapControl

Geocode services are useful for spatializing many types of business data. There are numerous services to choose from, but often business data is riddled with mistypes and other issues that cause geocode failures. I was recently faced with a data set that contained nearly a 30% failure rate from a common geocode batch service and decided to see what I could do with Virtual Earth’s geocode service.

Using web.config for setup parameters

In the process I was faced with passing parameters from my Web.config into the project and down to my Silverlight xaml to be used in the Page.cs code behind. Silverlight has only indirect access to information in the appSettings of web.config. I eventually stumbled across this approach:

  1. Web.config: add key value pair to the appSettings
  2. Default.aspx.cs: Use Page_Load to get the parameters and pass to Xaml1.InitParameters
  3. App.cs: use initParams to get the parameters and send them to the Page.cs Page() method
  4. Page.cs: in the Page.cs Page method set class variables with the parameters

1. Web.config
The web.config file can be used to store parameters in the appSettings section. These parameters are then available as application wide settings. An additional advantage is the ability to change settings on the deploy server without a rebuild and redeploy each time.

<appSettings>
   <add key=”geocodeLimit” value=”10″ />
   <add key=”VETokenID” value=”Your VE ID” />
   <add key=”VETokenPass” value=”Your VE password” />
</appSettings>

2. Default.aspx.cs:
Use Default.aspx.cs Page_Load(object sender, EventArgs e) method to setup the commonService and get the client token: commonService.GetClientToken(tokenSpec); for accessing the VEGeocodeWebService. Also pull the “geocodeLimit parameter from the appSettings, ConfigurationManager.AppSettings["geocodeLimit"]. These two parameters, “clientToken” and “limit” are then setup as InitParameters for the asp:Silverlight Xaml page, ID=”Xaml1″. InitParameters is a string listing comma delimited key=value pairs. The end result is Xaml1.InitParameters = “clientToken=Your Token,limit=10″.

protected void Page_Load(object sender, EventArgs e)
{
  CommonService commonService = new CommonService();
  commonService.Url = "https://staging.common.virtualearth.net/find-30/common.asmx";
  commonService.Credentials = new System.Net.NetworkCredential(
           ConfigurationManager.AppSettings["VETokenID"],
           ConfigurationManager.AppSettings["VETokenPass"]);

  // Create the TokenSpecification object to pass to GetClientToken.
  TokenSpecification tokenSpec = new TokenSpecification();

  // Use the 'Page'object to retrieve the end-client's IPAddress.
  tokenSpec.ClientIPAddress = Page.Request.UserHostAddress;

  // The maximum allowable token duration is 480 minutes (8 hours).
  // The minimum allowable duration is 15 minutes.
  tokenSpec.TokenValidityDurationMinutes = 480;

  // Now get a token from the Virtual Earth Platform service
  // and pass it to the Silverlight control
  Xaml1.InitParameters = "clientToken=" + commonService.GetClientToken(tokenSpec);
  Xaml1.InitParameters += ",limit=" + ConfigurationManager.AppSettings["geocodeLimit"];
}

3. App.cs:

On the Silverlight side, the App.cs has access to initParams through the StartupEventArgs. These strings are then passed as method parameters for the Page(token, limit) method of Page.cs.

        public App()
        {
            this.Startup += this.Application_Startup;
            this.Exit += this.Application_Exit;
            this.UnhandledException += this.Application_UnhandledException;
            InitializeComponent();
        }

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            string token = e.InitParams["clientToken"];
            string limit = e.InitParams["limit"];
            this.RootVisual = new Page(token, limit);
        }

4. Page.cs:

Finally in the Page.cs Page(string token, string limit) method sets the clientToken and geocodeLimit class variables before InitializeComponents().

        private string clientToken;
        privatestring geocodeLimit;

        public Page(string token, string limit)
        {
            clientToken = token;
            geocodeLimit = limit;
            InitializeComponent();
        }

At this point my VE token and geocodeLimit are available for use locally in my codebehind for Page.xaml

AddressLookup
Once that is settled I can move on to an address lookup using VEGeocodingService. The basic approach as seen in the initial Page view is a TextBox for input, a TextBox for output, a small menu for selecting a confidence factor, and a button to start the geocode when everything is ready. Underneath these controls is a VirtualEarth.MapControl. Addresses can be copy/pasted from text, csv, or xls files into the input TextBox. After geocoding the output TextBox can be copy/pasted back to a text file.

Once the geocode Click event is activated the confidence selection is added as a new ConfidenceFilter(). Next a geocodeRequest is setup, new GeocodeRequest(), using the input TextBox addresses, which are either tab or comma delimited, one per line. In addition to the address, a table ID code is included to keep track of the address. This id is passed into the geocodeService.GeocodeAsync(geocodeRequest, id); so that I can use it in the callback to identify the result.

Here is the geocode button click event handler:

private void Geocode_Btn(object sender, RoutedEventArgs e)
{
    string id = "no id";
    ConfidenceFilter[] filters = new ConfidenceFilter[1];
    filters[0] = new ConfidenceFilter();
    if (confHigh.IsChecked == true) filters[0].MinimumConfidence =
       VEGeocodingService.Confidence.High;
    else if (confMed.IsChecked == true) filters[0].MinimumConfidence =
       VEGeocodingService.Confidence.Medium;
    else filters[0].MinimumConfidence =  VEGeocodingService.Confidence.Low;

    GeocodeRequest geocodeRequest = new GeocodeRequest();
    OutPutText.Text = "";

    // Set the credentials using a valid Virtual Earth token
    geocodeRequest.Credentials = new VEGeocodingService.Credentials();
    geocodeRequest.Credentials.Token = clientToken;

    string lf = Environment.NewLine;
    string[] addressQueries = InputText.Text.Split(new string[]{lf},
        StringSplitOptions.RemoveEmptyEntries);
    if (addressQueries.Length < int.Parse(geocodeLimit))
    {
        foreach (string query in addressQueries)
        {
            string[] fields = Regex.Split(query, @"(,)|(\t)");
            geocodeRequest.Query = fields[0] + "," + fields[2] + "," + fields[4];
            if (fields.Length >6) id = fields[6];
            GeocodeOptions geocodeOptions = new GeocodeOptions();
            geocodeOptions.Filters = filters;
            geocodeRequest.Options = geocodeOptions;
            if (InputText.Text != "")
            {
                GeocodeServiceClient geocodeService = new GeocodeServiceClient();
                geocodeService.GeocodeCompleted += new EventHandler
                     (geocodeService_GeocodeCompleted);
                geocodeService.GeocodeAsync(geocodeRequest, id);
            }
            else OutPutText.Text += "Please enter a valid Address\n";
        }
    }
    else
    {
        MessageBox.Show("Sorry, too many addresses.\n Only allowed "+geocodeLimit+
                " at a time.", "Error", MessageBoxButton.OK);
    }
}

void geocodeService_GeocodeCompleted(object sender, GeocodeCompletedEventArgs e)
{
    GeocodeResponse geocodeResponse = e.Result;
    if (geocodeResponse.Results.Length > 0)
    {
        for (int i = 0; i < 1; i++)
        {
            VEGeocodingService.GeocodeResult res = geocodeResponse.Results[i];
            if (res.Address.AddressLine.Length > 1 &&
                res.Address.Locality.Length > 0 &&
                res.Address.AdminDistrict.Equals("CO"))
            {
                OutPutText.Text += e.UserState + ":" + res.Address.AddressLine + "," +
                   res.Address.Locality + "," + res.Address.AdminDistrict + "," +
                   res.Address.PostalCode + "," + res.Locations[0].Latitude + "," +
                   res.Locations[0].Longitude + "\n";
                if (!VEMap.Children.Contains(geocodeLayer))
                {
                    VEMap.Children.Add(geocodeLayer);
                }
                // add an ellipse shape at the first geocoded location
                Ellipse point = new Ellipse();
                point.Width = pointRadius;
                point.Height = pointRadius;
                point.RenderTransformOrigin = new Point(0.5, 0.5);
                point.Fill = new SolidColorBrush(Colors.Red);
                point.Opacity = 1.0;
                point.MouseEnter += point_MouseEnter;
                point.MouseLeave += point_MouseLeave;
                point.MouseLeftButtonDown += point_MouseDown;
                ToolTipService.SetToolTip(point, e.UserState + ":" + res.DisplayName +
                " : match=" + res.MatchCodes[0] + " conf=" + res.Confidence +
                " method=" + res.Locations[0].CalculationMethod);
                loc.Latitude = geocodeResponse.Results[i].Locations[0].Latitude;
                loc.Longitude = geocodeResponse.Results[i].Locations[0].Longitude;
                MapLayer.SetMapPosition(point, loc);
                geocodeLayer.Children.Add(point);

                // Zoom the map to the location of the item.
                MapViewSpecification spec = new MapViewSpecification(loc, 11);
                VEMap.View = spec;
            }
            else
            {
                OutPutText.Text += e.UserState + ": No Result found\n";
            }
        }
    }
    else OutPutText.Text += e.UserState + ": No Result found\n";
}

The callback, geocodeService_GeocodeCompleted(object sender, GeocodeCompletedEventArgs e), should have a GeocodeResponse with information about the matched addresses, any address modifications made by the parser, and the all important latitude, longitude. GeocodeCompletedEventArgs also holds the address id as a UserState object which I can cast to string.

The result may have several “finds” in its Results array, but I choose to just look at the first “find” returned for each address. By adding an ellipse to my VE MapControl and changing the VEMap.View I can examine the location of the “find” to see if it makes sense. Since I’m somewhat familiar with Denver, the idea is to look at a result and decide if it is junk or not, at least in the more egregious cases. The results are also added to the Output TextBox.

The ToolTipservice is a simple way to add rollover labelling to the address points, but perhaps a more complex rollover would be useful. Adding a couple of events, MouseEnter and MouseLeave, to the point shape allows any variety of rollover affect desired. Here is a simple set of events to change the fill brush:

private void point_MouseEnter(object sender, MouseEventArgs e)
{
    Shape s = sender as Shape;
    oldFill = (SolidColorBrush)s.Fill;
    s.Fill = new SolidColorBrush(Colors.Cyan);
}

private void point_MouseLeave(object sender, MouseEventArgs e)
{
    Shape s = sender as Shape;
    s.Fill = oldFill;
}

The points are available for viewing as a shape layer on top of the VE MapControl, but what if I can see how I should change the lat,lon location to make it more accurate. What I need is a drag capability to pick an address point and drag it to a new location.

This was a little more complicated than the simple rollover event. To start with I add a MouseLeftButtonDown event to the address ellipse. Normally you would also have a MouseMove and MouseLeftButtonUp event to help with the drag. However, VE MapControl complicates this approach masking move events with its pan event.

The solution that I found is to temporarily overide the VEMap.MousePan and VEMap.MouseLeftButtonUp with methods specific to the address point. The new point_Pan method sets its MapMouseEventArgs “handled” property to “true”, preventing the event from bubbling up to the VEMap pan. The VEMap.ViewportPointToLocation is used to change the screen x,y point to a lat,lon location as the mouse moves across the map. Once the MouseLeftButtonUp fires we can drop our shape at the new location and handle some book keeping in the Output TextBox.

Leaving the fill color “Green” indicates that this point has been relocated.It is also important to remove the temporary Mouse event handlers so that Map pan will work again.

private void point_MouseDown(object sender, MouseEventArgs e)
{
    selectedShape = sender as Shape;
    VEMap.MousePan += new EventHandler
(point_Pan);
    VEMap.MouseLeftButtonUp += point_Up;
}

private void point_Pan(object sender,  MapMouseEventArgs e)
{
    if (selectedShape != null)
    {
        e.Handled = true;
        Point p = e.ViewportPoint;
        p.X -= selectedShape.Width * 0.5;
        p.Y -= selectedShape.Height * 0.5;
        loc = VEMap.ViewportPointToLocation(p);
        MapLayer.SetMapPosition(selectedShape, loc);
    }
}

private void point_Up(object sender, MouseEventArgs e)
{
    if (selectedShape != null)
    {
        selectedShape.Fill = new SolidColorBrush(Colors.Green);
        string info = ToolTipService.GetToolTip(selectedShape) as string;
        string id = info.Split(':')[0];
        int startpos = OutPutText.Text.IndexOf(id);
        int endpos = OutPutText.Text.IndexOf("\n", startpos);
        string oldrec = OutPutText.Text.Substring(startpos, endpos - startpos);
        loc = MapLayer.GetMapPosition(selectedShape);
        string newrec = id + ":New Location,Denver,CO,,"+loc.Latitude+","+loc.Longitude;
        ToolTipService.SetToolTip(selectedShape, newrec);
        OutPutText.Text = OutPutText.Text.Replace(oldrec, newrec);
        selectedShape = null;
        VEMap.MousePan -= new EventHandler
(point_Pan);
        VEMap.MouseLeftButtonUp -= point_Up;
    }
}

Summary:

This little project covers three useful areas.

  1. A method of passing Web.Config parameters in to the xaml code behind for a Silverlight MapControl project.
    This is useful for deployment on multiple servers with differing needs as well as locating important credentials in a single place.
  2. Setting up a VEGeocodingService and making use of resulting locations in a Silverlight MapControl. Geocoding is a common need and the VE geocoding appears to be adequate. I noticed some strange behaviors when compared to Google. I was especially concerned that the parser seems to modify the address until it finds something that works, in fact anything that works, even locations in other states! The only response indicator that something was being modified is the MatchCodes[]
  3. Looking at some event driven interaction with new shape layer objects. Event driven interaction is a big plus, with granular events down to individual geometry shapes a map can become a live form for selection and edits. This is why I find Silverlight so attractive. It duplicates the event driven capability pioneered a decade ago in the SVG spec.

Comments are closed.