Geolocation with the Esri Silverlight API

Most modern browsers have support for the W3C geolocation API which allows you to access the physical location of the network connection for the user accessing your site. This is handy when you want to show or track the location and use it within your application. Access to this data is with JavaScript but as you can communicate between JavaScript and Silverlight you can also take advantage of the geolocation data in your Silverlight applications.

The first thing to do is add the JavaScript code to get the geolocation data (note that the browser will prompt the user for access). Here I’m using the watchPosition function so that each time the location changes we can update our display.

function init() {
    //Check if browser supports W3C Geolocation API
    if (navigator.geolocation) {
        navigator.geolocation.watchPosition(success);
    } 
}

function success(position) {
    var control = document.getElementById("SlControl");
    control.Content.SilverlightControl.UpdateLocation(position.coords.longitude,
                                                        position.coords.latitude, 
                                                        position.coords.accuracy);
}

“SlControl” is the id of the Silverlight plugin host object.

<object data="data:application/x-silverlight-2," id="SlControl" ...

SilverlightControl is the name assigned from the register method in our Silverlight code and UpdateLocation is the name of the method we need to invoke.

public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();

        HtmlPage.RegisterScriptableObject("SilverlightControl", this);
    }

    [ScriptableMember]
    public void UpdateLocation(double longitude, double latitude, double accuracyInMeters)
    {
        ((LocationLayer)MyMap.Layers["Location"]).SetLocation(longitude, latitude, accuracyInMeters);
    }
}

In UpdateLocation we pass the values from JavaScript through to our layer object and use them or abuse them as we want. For this example I display the current location along with the accuracy and a history of the locations. The layer code is

/// <summary>
/// Represents a location and its accuracy if set
/// </summary>
public class LocationLayer : GraphicsLayer
{
    static SpatialReference _webMercatorSref = new SpatialReference(102100);
    static SpatialReference _wgs84Sref = new SpatialReference(4326);
    static ESRI.ArcGIS.Client.Projection.WebMercator _mercator = new ESRI.ArcGIS.Client.Projection.WebMercator();

    PointCollection _locationHistory;

    public LocationLayer()
    {
        Renderer = new SimpleSymbolRenderer();
        _locationHistory = new PointCollection();
    }
        
    /// <summary>
    /// Sets the location for the layer.
    /// </summary>
    /// <param name="longitude"></param>
    /// <param name="latitude"></param>
    /// <param name="accuracyInMeters"></param>
    public void SetLocation(double longitude, double latitude, double accuracyInMeters)
    {
        Graphics.Clear();

        // Check the SR of the Map control. This could be replaced with a generic 
        // auto project behavior or attached property
        if (SpatialReference.AreEqual(Map.SpatialReference, _wgs84Sref, true))
        {
            AddLocation(new MapPoint { X = longitude, Y = latitude, SpatialReference = _wgs84Sref }, accuracyInMeters);
        }
        else if (SpatialReference.AreEqual(Map.SpatialReference, _webMercatorSref, true))
        {
            AddLocation((MapPoint)_mercator.FromGeographic(new MapPoint { X = longitude, Y = latitude }), accuracyInMeters);
        }
        else
        {
            GeometryServer.Project(new[] { new MapPoint { X = longitude, Y = latitude, SpatialReference = _wgs84Sref } },
                                    Map.SpatialReference,
                                    accuracyInMeters,
                                    (graphics, userToken) => { AddLocation((MapPoint)graphics.First().Geometry, (double)userToken); });
        }
    }

    void AddLocation(MapPoint geometry, double accuracyInMeters)
    {
        _locationHistory.Add(geometry);

        var history = new Polyline();
        history.Paths.Add(_locationHistory);
        Graphics.Add(new Graphic { Geometry = history });

        var locationGraphic = new Graphic { Geometry = geometry };
        locationGraphic.Attributes.Add("X", geometry.X);
        locationGraphic.Attributes.Add("Y", geometry.Y);
        Graphics.Add(locationGraphic);

        if ((int)accuracyInMeters == 0)
        {
            Refresh();
            return;
        }

        GeometryServer.Buffer(new[] { geometry }, new[] { accuracyInMeters }, LinearUnit.Meter, accuracyInMeters,
                            (geo, userToken) =>
                            {
                                var accuracyGraphic = new Graphic { Geometry = geo };
                                accuracyGraphic.Attributes.Add("X", geo.Extent.GetCenter().X);
                                accuracyGraphic.Attributes.Add("Y", geo.Extent.GetCenter().Y);
                                accuracyGraphic.Attributes.Add("AccuracyInMeters", (double)userToken);
                                Graphics.Insert(0, accuracyGraphic);
                                Map.ZoomTo(geo);
                            });
    }
}

I also use some helper classes for rendering the graphics and calling the geometry service methods. These are

internal sealed class SimpleSymbolRenderer : IRenderer
{
    public SimpleSymbolRenderer(MarkerSymbol markerSymbol, LineSymbol lineSymbol, FillSymbol fillSymbol)
    {
        MarkerSymbol = markerSymbol;
        LineSymbol = lineSymbol;
        FillSymbol = fillSymbol;
    }

    public SimpleSymbolRenderer()
        : this(new SimpleMarkerSymbol(), new SimpleLineSymbol(), new SimpleFillSymbol())
    {
    }

    public MarkerSymbol MarkerSymbol { get; set; }

    public LineSymbol LineSymbol { get; set; }

    public FillSymbol FillSymbol { get; set; }

    public Symbol GetSymbol(Graphic graphic)
    {
        var geometry = graphic.Geometry;

        if (graphic.Symbol != null) return graphic.Symbol;

        if (geometry is MapPoint)
            return MarkerSymbol;
        if (geometry is Polyline)
            return LineSymbol;
        if (geometry is Polygon || geometry is Envelope)
            return FillSymbol;
        return null;
    }
}
internal static class GeometryServer
{
    public static string Url = @"http://tasks.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer";

    static GeometryService CreateService()
    {
        var geometryService = new GeometryService(Url);
        geometryService.Failed += (sender, e) => OnFailed(e.Error);

        return geometryService;
    }

    static void OnFailed(Exception exception)
    {
        // Do something with this
    }

    public static void Buffer(IEnumerable<Geometry> geometries, IEnumerable<Double> bufferDistances, LinearUnit unit, object userToken, Action<Geometry, object> bufferComplete)
    {
        Buffer(GeometriesToGraphics(geometries), bufferDistances, unit, userToken, bufferComplete);
    }

    public static void Buffer(IList<Graphic> graphics, IEnumerable<Double> bufferDistances, LinearUnit unit, object userToken, Action<Geometry, object> bufferComplete)
    {
        var service = CreateService();

        service.BufferCompleted += (sender, e) =>
        {
            if (e.Results.Any())
                bufferComplete(e.Results.First().Geometry, e.UserState);
        };

        var spatialReference = graphics.First().Geometry.SpatialReference;

        var bufferParams = new BufferParameters
        {
            BufferSpatialReference = spatialReference,
            OutSpatialReference = spatialReference,
            UnionResults = true,
            Unit = unit
        };

        bufferParams.Features.AddRange(graphics);

        foreach (var distance in bufferDistances)
            bufferParams.Distances.Add(distance);

        service.BufferAsync(bufferParams, userToken);
    }

    public static void Project(IList<Geometry> geometries, SpatialReference spatialReference, object userToken, Action<IList<Graphic>, object> projectComplete)
    {
        Project(GeometriesToGraphics(geometries), spatialReference, userToken, projectComplete);
    }

    public static void Project(IList<Graphic> graphics, SpatialReference spatialReference, object userToken, Action<IList<Graphic>, object> projectComplete)
    {
        var service = CreateService();

        service.ProjectCompleted += (sender, e) =>
        {
            if (e.Results.Any())
                projectComplete(e.Results, e.UserState);
        };

        service.ProjectAsync(graphics, spatialReference, userToken);
    }

    static IList<Graphic> GeometriesToGraphics(IEnumerable<Geometry> geometries)
    {
        return geometries.Select(geometry => new Graphic { Geometry = geometry }).ToList();
    }
}

The xaml is very basic

<Grid x:Name="LayoutRoot" Background="White">
    <esri:Map x:Name="MyMap" WrapAround="True">
        <esri:ArcGISTiledMapServiceLayer ID="MyLayer"
            Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer" />
        <local:LocationLayer ID="Location">
            <esri:GraphicsLayer.MapTip>
                <Border BorderBrush="LightGray" BorderThickness="0.5" Margin="10">
                    <StackPanel Orientation="Vertical" Margin="0" Background="White">
                        <StackPanel Height="1.5" Background="#FF00A9DA"></StackPanel>
                        <StackPanel Orientation="Vertical" Margin="6">
                            <TextBlock Text="{Binding [X], StringFormat='X \{0\}'}" />
                            <TextBlock Text="{Binding [Y], StringFormat='Y \{0\}'}" />
                            <TextBlock Text="{Binding [AccuracyInMeters], StringFormat='Accurate to \{0\}m'}" />
                        </StackPanel>
                    </StackPanel>
                </Border>
            </esri:GraphicsLayer.MapTip>
        </local:LocationLayer>
    </esri:Map>
</Grid>

When all put together the output looks something like this.

Capture2

Advertisements

12 comments

    1. Hi Andrew, unfortunately I am unable to post .zip files here due to a limitation with the blog provider. The code above is all of the main parts though, it just needs wiring into a Silverlight application and the host web page.

  1. Hi Dave, Thanks for the reply. Is it something you could perhaps email me, or post on my FTP site? I’m still learning Silverlight (and am using Visual Basic). While I understand most of what you have here, I’m having a bit of a hard time translating. If I had a complete & functional project to reference, I probably would not spend as much time spinning my wheels. If possilbe, you could email t andrewbowne@gmail.com. Thanks again.

    1. Dave,
      I’ve muddled my way thru your code and have it up and running on my site. Do you know if I have a GPS connected to my laptop if it will produce more accurate locations? I have changed the javascript code as follows: navigator.geolocation.watchPosition(success, fail,{enableHighAccuracy: true});
      However it does not seem to make a difference. Any thoughts?

      1. Hi Andrew, glad you got it working.

        The enableHighAccuracy flag is basically telling the code to use the most accurate reading it can but it depends on the hardware you are running. I think this is mainly focused on smartphones but from the sounds of it you are running the code on your laptop using Windows? Given this scenario it is likely that the GPS receiver data is not used by the browser.

        My post is really showing 2 different things. The first is how to access location data using the geolocation API and the second is how to display location data using the Silverlight API. For your scenario you can still take advantage of the second part but you may need to rework how that data gets fed into your application. Again there are a couple of approaches. Depending upon your hardware there may be a sensor API that you can utilise and then feed that data into the app or if you already know the data that is being streamed then you just need to pass it through.

        Let me know how you get on.
        Cheers,
        Dave

  2. Dave,
    Your assumptions are correct. Basically what I’m doing is creating an application that will allow users to edit assets (street lights, signs, etc.) in the field via Esri’s Silverlight API. Instead of having to constantly pan/zoom, I want the app to “follow” them. I don’t need accuracy down to sub-meter, but close enough to where it will reflect a somewhat decent location. They will be running this on Panasonic Toughbooks with external GPS connected via USB.

    Any thoughts or insights are greatly appreciated. Thanks again for your help.

      1. There are two main driving forces behind why I’m trying to use the Silverlight Web API:
        1. I want to have a zero install environment. That way I don’t have to worry about chasing down laptops to update software.
        2. I’d like to leverage AGS web-editing functionality. That way I don’t have to worry about (again) chasing down laptops to download and sync field data with ArcSDE.

        Call me lazy but all I want to do is leverage the capabilites of the software.

        I don’t believe ArcGIS Explorer or ArcGIS Mobile allow for editing ArcGIS Server Feature Services. (Correct me if I’m wrong) If ArcMap could, I would go that route as all of the laptops have ArcMap installed on them.

        I checked out the Sensor API and there is one in particular for Location but there are no drivers out there that support our GPS units.

  3. Hi Andrew,
    you can use editing in ArcGIS Mobile using a Mobile Service (just check the mobile access in the service capabilities). There is also the ability to create a custom application using the mobile project center but this would mean you have the application deployed on each machine which is what you are trying to avoid. Are they constrained to those devices? That is the major issue you have as if so then ArcGIS mobile is probably your best option.

    ArcMap does allow for editing using Feature Services too assuming you are in a connected environment. That doesn’t solve your initial problem of reading the GPS data though.

    Cheers, Dave

  4. Dave,
    Is there a way I could also get an example of this emailed to me? I’m trying to do something similar, but by creating an add-in for ESRI’s out of the box silverlight viewer. I think taking a look at your solution, all together, would be helpful. Thanks!

    1. Hi Karen,

      I don’t have the solution for this anymore unfortunately though you should be able to reconstruct something similar using the snippets from the post.

      Cheers,
      Dave

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s