Month: September 2011

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

Adding a FeatureLayer by Name

Feature layers are immensely useful and powerful types that can be used within your Esri web applications. At a basic level they can be thought of as a GraphicsLayer with an associated QueryTask. The usual way of adding a FeatureLayer would be to reference the url of the layer you want to use. This url is composed of the map service endpoint and the layer id e.g.http://server/ArcGIS/rest/services/name/MapServer/0 where 0 is the layer id.

What we will look at now is how we can add a FeatureLayer by referencing the layer name instead of the id. The reason that this would be useful is that this would protect your layer from failing to load if the underlying map document that has been published as a service has had its layer order changed as this would mean that the layer id would change too.

To achieve this functionality we can query the REST endpoint of the service, lookup the service information and then use this to determine the layer id that corresponds to the supplied layer name. This technique can be used to retrieve all of the map service information but here I will just be returning the layer id and name.

Here’s the code to create the FeatureLayer

public static class FeatureLayerExtensions
{
    public static void FromLayerNameEndpoint(string mapServiceLayerNameEndpoint,
                                                Action<ESRI.ArcGIS.Client.FeatureLayer> onLayerCreated)
    {
        var featureLayer = new ESRI.ArcGIS.Client.FeatureLayer();

        featureLayer.GetLayerId(mapServiceLayerNameEndpoint,
                                (fLayer, url) =>
                                {
                                    fLayer.Url = url;
                                    if (onLayerCreated != null) onLayerCreated(fLayer);
                                });
    }

    public static void GetLayerId(this ESRI.ArcGIS.Client.FeatureLayer featureLayer,
                                    string endpoint,
                                    Action<ESRI.ArcGIS.Client.FeatureLayer, string> onLayerIdFound)
    {
        featureLayer.GetLayerId(endpoint, string.Empty, onLayerIdFound);
    }

    public static void GetLayerId(this ESRI.ArcGIS.Client.FeatureLayer featureLayer,
                                    string endpoint,
                                    string proxyUrl,
                                    Action<ESRI.ArcGIS.Client.FeatureLayer, string> onLayerIdFound)
    {
        var layerName = endpoint.TrimEnd('/').Split('/').Last();
        var uri = endpoint.TrimEnd('/').Substring(0, endpoint.TrimEnd('/').LastIndexOf('/'));

        var request = new WebClient();
        request.OpenReadCompleted += (sender, e) =>
        {
            // Using the built in Json serializer but you could use something else e.g. Json.NET
            var s = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(LayerInfo));
            var layers = ((LayerInfo)s.ReadObject(e.Result)).Layers;

            foreach (var layer in layers)
            {
                if (string.Equals((string)layer.Name, layerName, StringComparison.OrdinalIgnoreCase))
                {
                    if (onLayerIdFound != null) onLayerIdFound(featureLayer, string.Format("{0}/{1}", uri, layer.Id));
                    return;
                }
            }

            throw new InvalidOperationException(string.Format("A layer with the name '{0}' was not found in the map service.", layerName));
        };

        if (string.IsNullOrEmpty(proxyUrl))
            request.OpenReadAsync(new Uri(EnsureJsonResponseFormatUrl(uri)));
        else
            request.OpenReadAsync(new Uri(string.Format("{0}?{1}", proxyUrl, EnsureJsonResponseFormatUrl(uri))));
    }
        
    static string EnsureJsonResponseFormatUrl(string url)
    {
        if (url == null) throw new ArgumentNullException("url");

        if (url.IndexOf("f=json") == -1)
        {
            if (url.IndexOf('?') == -1)
                url += "?";
            else
                url += "&";
            return url + "f=json";
        }
        else
            return url;
    }
}

and the data contract that is returned from the endpoint

[DataContract]
public class LayerInfo
{
    [DataMember(Name = "layers")]
    public LayerInfoItem[] Layers { get; set; }

}

/// <summary>
/// Contains layer properties from the MapServer REST endpoint.
/// Could be extended to return all values available e.g. type, minScale etc. 
/// </summary>
[DataContract]
public class LayerInfoItem
{
    [DataMember(Name = "id")]
    public int Id { get; set; }

    [DataMember(Name = "name")]
    public string Name { get; set; }
}

and how to call it

var url = "http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Earthquakes/EarthquakesFromLastSevenDays/MapServer/Earthquakes from last 7 days";
FeatureLayerExtensions.FromLayerNameEndpoint(url,
                                                (featureLayer) =>
                                                {    
                                                    // TODO : any other initialization 
                                                    featureLayer.Opacity = 0.6;
                                                    MyMap.Layers.Add(featureLayer);
                                                });

Now when you run your application you will see the FeatureLayer added to your map. If you have fiddler running you can see the requests being made. I’d encourage you to dig into these to help understand the process involved.

A final point; A FeatureLayer cannot have its Url property set after the layer has been initialized. This is important here as it means that we need to create and add the layer in code rather than with markup.

Fluent Where Clause

I like working with fluent APIs and using the fluent syntax with LINQ, it makes more sense to me and is easier to follow. With this in mind I thought it would be fun to create a simple fluent style where clause for ArcGIS queries or DefinitionExpressions. The main benefits of this approach are to enhance readability and to reduce runtime errors from using magic strings as usually you would construct these queries using string.Format(…). This is by no means a fully featured implementation but it serves as a starting point for anyone that is interested and I may extend it in the future.

At the center of this I created a new class called WhereClause. Within this class are a number of standard operations that you would use when constructing a query such as comparing values and joining statements. From each function I return the WhereClause object so that subsequent functions can be chained. The code looks like.

public class WhereClause
{
    StringBuilder _whereClause;
    int _nestLevel;

    const string OpenBracket = "(";
    const string CloseBracket = ")";

    const string EqualsOperator = "=";
    const string GreaterThanOperator = ">";
    const string LessThanOperator = "<";
    const string InOperator = "in ({0})";
    const string InForStringOperator = "in ('{0}')";

    const string AndOperator = "AND";
    const string OrOperator = "OR";
    const string LikeOperator = "LIKE '{0}%'";

    const string Separator = ",";
    const string SeparatorForString = "','";

    public static WhereClause Create()
    {
        return new WhereClause();
    }

    WhereClause()
    {
        _whereClause = new StringBuilder();
    }

    public WhereClause For<T>(T forObject, Expression<Func<string>> property) where T : class
    {
        AddCloseBracket();
        PropertyInfo propertyInfo;
        if (property.Body is MemberExpression)
        {
            propertyInfo = ((MemberExpression) property.Body).Member as PropertyInfo;
        }
        else
        {
            propertyInfo = ((MemberExpression) ((UnaryExpression)property.Body).Operand).Member as PropertyInfo;
        }
        if (propertyInfo != null) propertyInfo.SetValue(forObject, _whereClause.ToString().Trim(), null);

        return this;
    }

    public WhereClause Field<T>(Expression<Func<T>> expression)
    {
        return Field(GetPropertyName(expression));
    }

    public WhereClause Field(string name)
    {
        AddOpenBracket();
        AppendPart(name);
        return this;
    }        

    public WhereClause And()
    {
        AddCloseBracket();
        AppendPart(AndOperator);
        return this;
    }

    public WhereClause AndField<T>(Expression<Func<T>> expression)
    {
        return AndField(GetPropertyName(expression));
    }

    public WhereClause AndField(string name)
    {            
        AppendPart(AndOperator);
        AppendPart(name);
        return this;
    }

    public WhereClause Or()
    {
        AddCloseBracket();
        AppendPart(OrOperator);
        return this;
    }

    public WhereClause OrField<T>(Expression<Func<T>> expression)
    {
        return OrField(GetPropertyName(expression));
    }

    public WhereClause OrField(string name)
    {
        AppendPart(OrOperator);
        AppendPart(name);
        return this;
    }

    public WhereClause Equals<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return Equals(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException("Collection should be of type list");

        AppendPart(EqualsOperator);
        AppendPart(value.ToString());
        return this;
    }

    public WhereClause Equals<T>(List<T> values)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return Equals(values.Cast<string>());

        AppendPart(string.Format(InOperator, string.Join(Separator, values)));
        return this;
    }

    WhereClause Equals(string value)
    {
        AppendPart(EqualsOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    WhereClause Equals(IEnumerable<string> values)
    {
        AppendPart(string.Format(InForStringOperator, string.Join(SeparatorForString, values)));
        return this;
    }

    public WhereClause GreaterThan<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return GreaterThan(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException();

        AppendPart(GreaterThanOperator);
        AppendPart(value.ToString());
        return this;
    }

    WhereClause GreaterThan(string value)
    {
        AppendPart(GreaterThanOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    public WhereClause GreaterThanOrEquals<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return GreaterThanOrEquals(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException();

        AppendPart(GreaterThanOperator + EqualsOperator);
        AppendPart(value.ToString());
        return this;
    }

    WhereClause GreaterThanOrEquals(string value)
    {
        AppendPart(GreaterThanOperator + EqualsOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    public WhereClause LessThan<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return LessThan(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException();

        AppendPart(LessThanOperator);
        AppendPart(value.ToString());
        return this;
    }

    WhereClause LessThan(string value)
    {
        AppendPart(LessThanOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    public WhereClause LessThanOrEquals<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return LessThanOrEquals(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException();

        AppendPart(LessThanOperator + EqualsOperator);
        AppendPart(value.ToString());
        return this;
    }

    WhereClause LessThanOrEquals(string value)
    {
        AppendPart(LessThanOperator + EqualsOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    public WhereClause StartsWith(string value)
    {
        AppendPart(string.Format(LikeOperator, value));
        return this;
    }

    void AppendPart(string part)
    {
        if (_whereClause.ToString().EndsWith(" "))
            _whereClause.Append(part);
        else
            _whereClause.Append(" " + part);
    }

    static string GetPropertyName<T>(Expression<Func<T>> expression)
    {
        var exp = (MemberExpression)expression.Body;
        return exp.Member.Name;
    }

    static bool IsIEnumerableOfT(Type type)
    {
        return type.GetInterfaces().Any(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>));
    }

    void AddOpenBracket()
    {
        AppendPart(OpenBracket);
        _nestLevel++;
    }

    void AddCloseBracket()
    {
        if (_nestLevel == 0) return;

        AppendPart(CloseBracket);
        _nestLevel--;
    }
}

In order to use this we need to do 3 things.

  1. Create a new instance of a WhereClause
  2. Add the statements we want to execute
  3. Apply the statement to a property

An example is

var queryTask = new QueryTask("http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Demographics/ESRI_Census_USA/MapServer/5");
queryTask.ExecuteCompleted += QueryTask_ExecuteCompleted;

var query = new Query { ReturnGeometry = true, OutSpatialReference = MyMap.SpatialReference };
query.OutFields.Add("*");

var states = new List<string> { "Florida", "Alaska", "Idaho", "Hawaii" };

// Example of using string values and chaining statements
WhereClause.Create()
    .Field("STATE_NAME").Equals(states)
    .Or()
    .Field("POP2007").LessThanOrEquals(1000000)
    .Or()
    .Field("STATE_NAME").StartsWith("Te")
    .For(query, () => query.Where);

queryTask.ExecuteAsync(query);

You should be able to see the 3 steps outlined above. The output from is looks like

( STATE_NAME in (‘Florida’,’Alaska’,’Idaho’,’Hawaii’) ) OR ( POP2007 <= 1000000 ) OR ( STATE_NAME LIKE ‘Te%’ )

query2

query2output

This has already eliminated some potential typo’s but you can also use your own object properties for the statement field names as long as they match the underlying data. The advantage of this is that any errors will throw compile time errors as opposed to runtime errors.

An example of this is

var featureLayer = MyMap.Layers["MyFeatureLayer"] as FeatureLayer;

if (featureLayer == null || featureLayer.InitializationFailure != null)
    return;

// The property values here could be from your view model
WhereClause.Create()
    .Field(() => Pop1990).GreaterThan(Pop1990) // Using the property name as the query column
    .AndField("CITY_NAME").StartsWith("Bi")    // Using text for the query column
    .Or()
    .Field("CITY_NAME").StartsWith("Water")
    .For(featureLayer, () => featureLayer.Where);

featureLayer.Update();

( Pop1990 > 20000 AND CITY_NAME LIKE ‘Bi%’ ) OR ( CITY_NAME LIKE ‘Water%’ )

query1

I’d be interested to hear what you think of this approach or any suggestions you may have. Thanks for reading.