Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

SQL Geometry Viewer

4.67/5 (18 votes)
11 Sep 2008CPOL5 min read 1   5.1K  
An on-screen visualizer for the SQL Geometry type
Image 1

Introduction

SQL Server 2008 includes two new data types: geography and geometry. Table columns of these data types hold data compatible to the Open Geospatial Consortium's standards. However, the tools supplied with SQL Server do not provide a way to visualize the data. Enter this little tool, which does exactly that for the geometry type.

What the Tool Does

The tool accepts input in two modes: User Input and Database Input. In both input modes, the given geometries are added to a grid for tabular view, and to a canvas for visualization.

User Input

In user input mode, we may type a WKT description of a geometry, for instance, POLYGON((0 0, 50 0, 50 50, 0 50, 0 0)), which describes a 50x50 square with its bottom-left corner at the origin of the axes.

Database Input

In database input mode, after we have established a connection to a SQL Server 2008 database, we type a SQL query returning at most two columns of data. In the case of two columns, the first column is used to identify the geometry, and the second is the geometry itself. In the case of one column, the column itself is the geometry, identified by an auto-generated GUID.

Background

The inspiration for this little tool was Simon Sabin's SpatialViewer. Indeed, a tiny portion of Simon's code has been copied verbatim. SpatialViewer provides the ability to generate WKT from the geometries drawn with the mouse on a canvas and to show it to the user. However, it suffers from two limitations:

  • It is incapable of accepting input from a database (a shortcoming which can be easily corrected); and
  • It is written for Windows Forms, therefore failing to take advantage of WPF's excellent data binding and drawing capabilities.

Dissecting the Code

The main point of interest is the file GeometryInfo.cs, which consists of two classes: GeometryInfo and GeometryCollection. The former encapsulates a geometry, and provides properties that return the geometry's area, its length, and whether it is a valid geometry or not. The geometry itself is stored in the Data property, which is of type SqlGeometry.

The Geometry read-only property returns the geometry as an instance of the WPF Geometry class. To achieve this, the Decode() method parses the geometry's WKT and returns the appropriate Geometry instance:

C#
private Geometry Decode(SqlGeometry g)
{
    PathGeometry result = new PathGeometry();

    switch (g.STGeometryType().Value.ToLower())
    {
        case "point":
            PathFigure pointFig = new PathFigure();

            pointFig.StartPoint = new Point(g.STX.Value - 2, g.STY.Value - 2);
            LineSegment pointLs = new LineSegment(new Point(g.STX.Value + 2, 
                                      g.STY.Value + 2), true);
            pointFig.Segments.Add(pointLs);
            result.Figures.Add(pointFig);

            pointFig = new PathFigure();
            pointFig.StartPoint = new Point(g.STX.Value - 2, g.STY.Value + 2);
            pointLs = new LineSegment(new Point(g.STX.Value + 2, 
                                      g.STY.Value - 2), true);
            pointFig.Segments.Add(pointLs);
            result.Figures.Add(pointFig);

            return result;
        case "polygon":
            string cmd = new string(g.STAsText().Value).Trim().Substring(8);
            string[] polyArray = (cmd.Substring(1, cmd.Length - 2) + 
                                  ", ").Split('(');
            var polys = from s in polyArray
                        where s.Length > 0
                        select s.Trim().Substring(0, s.Length - 3);

            PathFigure fig;
            foreach (var item in polys)
            {
                fig = new PathFigure();
                var polyPoints = from p in item.Split(',')
                                 select p.Trim().Replace(" ", ",");
                fig.StartPoint = Point.Parse(polyPoints.ElementAt(0));
                for (int i = 1; i < polyPoints.Count(); i++)
                {
                    LineSegment ls = new LineSegment(
                                Point.Parse(polyPoints.ElementAt(i)), true);
                    fig.Segments.Add(ls);
                }
                result.Figures.Add(fig);
            }

            return result;
        case "linestring":
            PathFigure lsfig = new PathFigure();
            lsfig.StartPoint = new Point(g.STPointN(1).STX.Value, 
                                         g.STPointN(1).STY.Value);
            for (int i = 1; i <= g.STNumPoints(); i++)
            {
                LineSegment ls = new LineSegment();
                ls.Point = new Point(g.STPointN(i).STX.Value, 
                                     g.STPointN(i).STY.Value);
                lsfig.Segments.Add(ls);
            }
            result.Figures.Add(lsfig);

            return result;
        case "multipoint":
        case "multilinestring":
        case "multipolygon":
        case "geometrycollection":
            GeometryGroup mpG = new GeometryGroup();
            for (int i = 1; i <= g.STNumGeometries().Value; i++)
                mpG.Children.Add(Decode(g.STGeometryN(i)));

            return mpG;
        default:
            return Geometry.Empty;
    }
}

The Decode() method queries the geometry's type using the STGeometryType() method of the SqlGeometry class. If the geometry is a geometry collection (one of MultiPoint, MultiLineString, MultiPolygon, or GeometryCollection), it creates a GeometryGroup and adds to it the collection's geometries by recursively calling Decode() on each of the collection's children. Otherwise, it creates a PathGeometry that represents the given SqlGeometry. Note that points are added as crosses, with an envelope of side 4.

The GeometryCollection class inherits DependencyObject to provide three read-only dependency properties:

  • TranslateX and TranslateY, which give the offset that the geometry collection must be translated in order for its top left corner to be placed at the axes origin; and
  • BoundingBox, which returns the geometry collection's envelope - the smallest rectangle that contains the whole geometry.

The class exposes a fourth property, Geometries, of type ObservableCollection<GeometryInfo>. When the underlying collection changes, the other three properties are updated. This is accomplished by attaching an event handler to the collection's CollectionChanged event:

C#
void GeometriesCollectionChanged(object sender, 
     System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
    if (Geometries.Count > 0)
    {
        var minX = (from p in Geometries
                    select p.Data.STEnvelope().STPointN(1).STX.Value).Min();
        var maxX = (from p in Geometries
                    select p.Data.STEnvelope().STPointN(2).STX.Value).Max();
        var minY = (from p in Geometries
                    select p.Data.STEnvelope().STPointN(1).STY.Value).Min();
        var maxY = (from p in Geometries
                    select p.Data.STEnvelope().STPointN(4).STY.Value).Max();
        SetValue(BoundingBoxPropertyKey, new Rect(minX, minY, maxX - minX, maxY - minY));
        SetValue(TranslateXPropertyKey, -BoundingBox.TopLeft.X);
        SetValue(TranslateYPropertyKey, -BoundingBox.TopLeft.Y);
    }
    else
    {
        SetValue(BoundingBoxPropertyKey, new Rect(0, 0, 0, 0));
        SetValue(TranslateXPropertyKey, 0.0);
        SetValue(TranslateYPropertyKey, 0.0);
    }
}

Using the Tool

Use the tab pages at the bottom of the main window to switch between the User Input mode and the Database Input mode.

In the User Input mode, type the geometry commands, each on a new line. These are parsed as you type, and the parse result is displayed below the commands. If the geometry is valid, click the Add button to add it to the viewer. If it is not, but a valid geometry can be constructed from it, click the Add Valid button.

In the Database Input mode, first establish a connection to a SQL Server 2008 database that contains tables with spatial data (connection status is displayed on the status bar). Then, type a SQL command following the guidelines in the introduction, and finally click the Add button.

Note that adding a geometry does not remove the geometries already added. To delete a geometry, select it from the grid and press the Delete button. To clear the geometry list completely, click the Clear Canvas button on the toolbar.

Use the slider on the toolbar to zoom in or out. The zoom factor ranges from 0.1 to 30.

Click and drag the canvas to pan.

Points of Interest

A couple of interesting challenges were implementing the canvas and implementing the panning. Solving one problem turns out to solve the other. The canvas comprises of a Border, which contains a Canvas, which contains an ItemsControl. The ItemsControl is bound to a GeometryCollection and is styled such that its ItemsPanel contains a Grid (for absolute positioning) and so that each item presents itself as a Path whose geometry is bound to the item's Geometry property:

XML
<Style TargetType="ItemsControl">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <Grid>
                    <Grid.RenderTransform>
                        <TransformGroup>
                            <TranslateTransform 
                                X="{Binding Path=TranslateX}" 
                                Y="{Binding Path=TranslateY}" />
                        </TransformGroup>
                    </Grid.RenderTransform>
                    <Grid.LayoutTransform>
                        <TransformGroup>
                            <ScaleTransform 
                                CenterX="0" CenterY="0" 
                                ScaleX="{Binding ElementName=sldZoom, 
                                       Path=Value}" 
                                ScaleY="{Binding ElementName=sldZoom, 
                                       Path=Value}" />
                        </TransformGroup>
                    </Grid.LayoutTransform>
                </Grid>
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
</Style>

<DataTemplate DataType="{x:Type local:GeometryInfo}">
    <Path Data="{Binding Path=Geometry}" 
          Stroke="Black" StrokeThickness="0.2" 
          Fill="{Binding Path=Fill}">
        <Path.ToolTip>
            <StackPanel Width="250" TextBlock.FontSize="12">
                <TextBlock FontWeight="Bold" Text="
                     {Binding Path=Id}" />
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Area: " />
                    <TextBlock Text="{Binding Path=Area}" />
                    <TextBlock Text=" units" />
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Length: " />
                    <TextBlock Text="{Binding Path=Length}" />
                    <TextBlock Text=" units" />
                </StackPanel>
            </StackPanel>
        </Path.ToolTip>
    </Path>
</DataTemplate>

Finally, the Border handles the PreviewMouseUp, PreviewMouseDown, and PreviewMouseMove events so that they apply an appropriate TranslateTransform to the ItemsControl to implement panning. Note that a MatrixTransform and a TranslateTransform is applied to the Border in order not to draw the canvas upside down (in a Cartesian coordinate system, Y-coordinates increase as you move upwards, whereas in the WPF coordinate system, Y-coordinates increase as you move towards the bottom of the screen):

XML
<Border Margin="4,0,4,4" BorderThickness="0.5,0.5,0.5,0.5" 
        BorderBrush="{DynamicResource 
                    {x:Static SystemColors.ActiveCaptionTextBrushKey}}" 
        Background="Transparent" Name="masterCanvas" 
        PreviewMouseMove="masterCanvas_PreviewMouseMove" 
        PreviewMouseDown="masterCanvas_PreviewMouseDown" 
        PreviewMouseUp="masterCanvas_PreviewMouseUp">
    <Border.RenderTransform>
        <TransformGroup>
            <MatrixTransform Matrix="1,0,0,-1,0,0" />
            <TranslateTransform 
                X="0" 
                Y="{Binding ElementName=masterCanvas, Path=ActualHeight}" />
        </TransformGroup>
    </Border.RenderTransform>
    <Canvas ClipToBounds="True">
        <ItemsControl Name="drawingCanvas" 
                  IsTabStop="False" 
                  ItemsSource="{Binding Path=Geometries}">
            <ItemsControl.RenderTransform>
                <TranslateTransform X="{Binding Path=BoundingBox.X}"
                                    Y="{Binding Path=BoundingBox.Y}" />
            </ItemsControl.RenderTransform>
        </ItemsControl>
    </Canvas>
</Border>

History

  • 2008-9-8: First version released
  • 2008-9-9: Demo updated 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)