Diederik Krols

The XAML Brewer

Displaying spatial data in WPF: from SqlGeometry to PathGeometry

This article explains how to visualize spatial data (e.g. from SQL Server 2008) in Windows Presentation Foundation without using 3rd party components or proprietary formats. We'll build a little form that allows us to enter test data in the Well-Known Text format (WKT) - manually or via copy/paste, e.g. from SQL Management Studio. A Draw button will convert our input to WPF graphics, and display it. Here's how the application looks like:

It's probably a waste of time to do the validation of the input text and the parsing of its structure ourselves, since the native spatial SQL Server .NET UDTs -SqlGeometry and SqlGeography- are specialized in that. These types are stored in the Microsoft.SqlServer.Types assembly, so we should make a reference to that one in our project. On the user interface side, the best candidate type to visualize spatial data in WPF is without any doubt the Geometry class, which represents a composite 2D-shape. To create a WPF version of spatial data, we read the WKT format and use it to initialize a SqlGeometry instance. Then we call some of the OGC-functions to break the SqlGeometry Object Model down into a PathGeometry Structure. For ease of use, it makes sense to wrap this functionality in extension methods for SqlGeometry. Here's the class:

namespace U2UConsult.DockOfTheBay

{

    using System.Windows;

    using System.Windows.Media;

    using System.Windows.Shapes;

    using Microsoft.SqlServer.Types;

 

    /// <summary>

    /// Extension Methods for SqlGeometry.

    /// </summary>

    public static class SqlGeometryExtensions

    {

        /// <summary>

        /// Translates a SqlGeometry into a Systems.Windows.Media.Geometry.

        /// </summary>

        public static Geometry AsWpfGeometry(this SqlGeometry sqlGeometry)

        {

            PathGeometry result = new PathGeometry();

 

            switch (sqlGeometry.STGeometryType().Value.ToLower())

            {

                case "point":

 

                    // Return a little 'X'

                    // (well: 'little' depends on the coordinate system ...)

                    PathFigure pointFigure = new PathFigure();

                    pointFigure.StartPoint = new Point(

                        sqlGeometry.STX.Value - .1,

                        sqlGeometry.STY.Value - .1);

                    LineSegment line = new LineSegment(

                        new Point(

                            sqlGeometry.STX.Value + .1,

                            sqlGeometry.STY.Value + .1),

                        true);

                    pointFigure.Segments.Add(line);

                    result.Figures.Add(pointFigure);

 

                    pointFigure = new PathFigure();

                    pointFigure.StartPoint = new Point(

                        sqlGeometry.STX.Value - .1,

                        sqlGeometry.STY.Value + .1);

                    line = new LineSegment(

                        new Point(

                            sqlGeometry.STX.Value + .1,

                            sqlGeometry.STY.Value - .1),

                        true);

                    pointFigure.Segments.Add(line);

                    result.Figures.Add(pointFigure);

 

                    return result;

 

                case "polygon":

 

                    // A Spacial Polygon is a collection of Rings

                    // A Ring is a Closed LineString

                    // So, return a PathFigure for each Ring

 

                    // Outer Ring

                    result.Figures.Add(LineStringToWpfGeometry(sqlGeometry.STExteriorRing()));

 

                    // Inner Rings

                    for (int i = 1; i <= sqlGeometry.STNumInteriorRing(); i++)

                    {

                        result.Figures.Add(LineStringToWpfGeometry(sqlGeometry.STInteriorRingN(i)));

                    }

 

                    return result;

 

                case "linestring":

 

                    // Return a PathFigure

                    result.Figures.Add(LineStringToWpfGeometry(sqlGeometry));

                    return result;

 

                case "multipoint":

                case "multilinestring":

                case "multipolygon":

                case "geometrycollection":

 

                    // Return a Group of Geometries

                    GeometryGroup geometryGroup = new GeometryGroup();

 

                    for (int i = 1; i <= sqlGeometry.STNumGeometries().Value; i++)

                    {

                        geometryGroup.Children.Add(sqlGeometry.STGeometryN(i).AsWpfGeometry());

                    }

 

                    return geometryGroup;

 

                default:

 

                    // Unrecognized Type

                    // Return an empty Geometry

                    return Geometry.Empty;

            }

        }

 

        /// <summary>

        /// Translates a SqlGeometry into a Systems.Windows.Shapes.Path.

        /// </summary>

        public static Path AsPath(this SqlGeometry sqlGeometry)

        {

            Path path = new Path();

            path.Data = sqlGeometry.AsWpfGeometry();

            return path;

        }

 

        /// <summary>

        /// Translates a LineString or a single Polygon Ring to a PathFigure.

        /// </summary>

        private static PathFigure LineStringToWpfGeometry(SqlGeometry sqlGeometry)

        {

            PathFigure result = new PathFigure();

 

            result.StartPoint = new Point(

                sqlGeometry.STPointN(1).STX.Value,

                sqlGeometry.STPointN(1).STY.Value);

 

            for (int i = 1; i <= sqlGeometry.STNumPoints(); i++)

            {

                LineSegment lineSegment = new LineSegment();

                lineSegment.Point = new Point(

                    sqlGeometry.STPointN(i).STX.Value,

                    sqlGeometry.STPointN(i).STY.Value);

                result.Segments.Add(lineSegment);

            }

 

            return result;

        }

    }

}

To use these extension methods, all we need to do is create a SqlGeometry instance with some data. Then we need to ensure it's valid against the OGC standards, so that the OGC compliant sql methods behave properly. Finally we call the conversion, like this:

// Make OGC Compliant

if (!sqlGeometry.STIsValid())

{

    sqlGeometry = sqlGeometry.MakeValid();

}

 

// Transformation Samples

Path path = sqlGeometry.AsPath();

Geometry geometry = sqlGeometry.AsWpfGeometry();

We end up with a Geometry that is expressed in the original spatial coordinates: latitude/longitude or X/Y against a specific SRID. So we need to translate and scale it, to project it to windows coordinates. Since we only visualize one shape, it suffices to let it stretch automatically in its container. By the way: don't forget to draw upside-down, because the origin of a Control is the upper left corner while the origin of a map is generally at the bottom left:

// Flip Y-coordinate

// Origin of a map is usually at the bottom left

path.LayoutTransform = new ScaleTransform(1, -1);

 

// Automate Translation & Inflation

path.Stretch = Stretch.Uniform;

 

Samples

I tested the code against a high number of possible (mostly Belgian) shapes. Here are some examples:

The river Zenne at the point where it leaves Brussels, heading to the north. An example of a LineString.
The province of Antwerp. An example of a MultiPolygon.
The province of Flemish Brabant. An example of a Polygon with an inner Ring.
The arrondissement Halle-Vilvoorde. An example of a Polygon.

 

Here's the full code for the WPF Window. To bring you up-to-speed immediately, it starts with a reduced shape (24 Points) of Belgium:

XAML:

<Window

   x:Class="U2UConsult.DockOfTheBay.SpatialSample"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="SQL Spatial to WPF Sample"

   Icon="/U2UConsult.DockOfTheBay;component/dotbay.png">

    <Grid>

        <Grid.RowDefinitions>

            <RowDefinition Height="*" />

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="*" />

            <ColumnDefinition Width="2*" />

        </Grid.ColumnDefinitions>

        <Button

           x:Name="DrawButton"

           Click="DrawButton_Click"

           Content="Draw"

           Grid.Row="0" Grid.Column="1"

           Margin="15"

           Height="32" Width="64"

           HorizontalAlignment="Left"

           VerticalAlignment="Bottom"

           />

        <!-- Reduced shape of Belgium -->

        <TextBox

           x:Name="GeometryTextBox"

           Margin="5"

           Grid.Row="0" Grid.Column="0"

           TextWrapping="Wrap"

           AcceptsReturn="True"

           AcceptsTab="True"

           Text="POLYGON ((

                    5.4695768356323242 49.499450206756592,

                    5.8744573593139648 49.576767921447754,

                    5.7810144424438477 49.959678173065186,

                    6.4083404541015625 50.333068847656250,

                    6.1721935272216800 50.550515174865723,

                    6.2783794403076172 50.616397857666016,

                    5.6911020278930664 50.761138916015625,

                    5.8341245651245117 51.168460845947266,

                    5.2405147552490234 51.261853218078613,

                    5.0372371673583984 51.485389232635500,

                    4.4786109924316406 51.480998992919922,

                    3.9020967483520508 51.198946952819824,

                    3.1825008392333984 51.361250877380371,

                    2.5581483840942383 51.093193054199219,

                    2.6278944015502930 50.814075946807861,

                    3.1747541427612305 50.752677917480469,

                    3.2816371917724609 50.526985168457031,

                    3.6048288345336914 50.489061832427979,

                    3.7020025253295900 50.300303936004639,

                    4.2115697860717773 50.269905090332031,

                    4.1991643905639648 49.960120201110840,

                    4.6723327636718750 49.985515117645264,

                    4.8746204376220700 50.151000022888184,

                    4.8553209304809570 49.794033050537109,

                    5.4695768356323242 49.499450206756592))"

           ToolTip="Place your Well-Known Text here ..."

           />

        <Border

           x:Name="DrawingCanvas"

           Padding="15"

           Grid.Row="0" Grid.Column="1"

           />

    </Grid>

</Window>

C#:

namespace U2UConsult.DockOfTheBay

{

    using System;

    using System.Windows;

    using System.Windows.Controls;

    using System.Windows.Media;

    using System.Windows.Shapes;

    using Microsoft.SqlServer.Types; // Add Reference !!!

 

    /// <summary>

    /// Demonstrates displaying SQL Server Spatial data in WPF.

    /// </summary>

    public partial class SpatialSample : Window

    {

        public SpatialSample()

        {

            InitializeComponent();

        }

 

        private void DrawButton_Click(object sender, RoutedEventArgs e)

        {

            try

            {

                // Read Well-Known Text

                SqlGeometry sqlGeometry = SqlGeometry.Parse(this.GeometryTextBox.Text);

 

                // Make OGC Compliant

                if (!sqlGeometry.STIsValid())

                {

                    sqlGeometry = sqlGeometry.MakeValid();

                }

 

                // Transform to Path

                Path path = sqlGeometry.AsPath();

 

                // Basic Properties

                path.Stroke = Brushes.Black;

                path.StrokeThickness = 1;

 

                // Polygons only ...

                //path.Effect = new DropShadowEffect() { Direction = 225 };

                //path.Fill = Brushes.DarkGreen;

 

                // Flip Y-coordinate

                // Origin of a map is usually at the bottom left

                path.LayoutTransform = new ScaleTransform(1, -1);

 

                // Automate Translation & Inflation

                path.Stretch = Stretch.Uniform;

 

                this.DrawingCanvas.Child = path;

            }

            catch (Exception ex)

            {

                // Input not valid

                this.DrawingCanvas.Child = new TextBlock()

                {

                    TextWrapping = TextWrapping.Wrap,

                    MaxHeight = 128,

                    VerticalAlignment = VerticalAlignment.Top,

                    Foreground = Brushes.Red,

                    Text = ex.Message

                };

            }

        }

    }

}

Comments are closed