A View Inside My Head

Jason's Random Thoughts of Interest
posts - 68, comments - 168, trackbacks - 10

SQL Server 2008: Spatial Data, Part 8

 

In this, the eighth part in a series on the new Spatial Data types in SQL Server 2008, I'll step away from the database and do a little spatial coding using .NET.

Redistributable .NET Library

Up to this point in the series, I have demonstrated a lot of interesting (?) things that you can do with the new Spatial data types (Geometry and Geography) in SQL Server 2008.  You might be thinking, "That's swell, and all, but I wish I could do some of that stuff without needing to be tethered to a database."  Well, you know what?  You can!

I mentioned in a previous post that the Spatial data types were implemented as SQLCLR User-Defined Types.  I've since been corrected by Isaac Kunen, who stated that they are more accurately described as System-Defined Types, with the difference being that these are automatically installed and available for use as part of SQL Server 2008, regardless of whether the ENABLE CLR bit has been activated.  Semantics aside, these types are merely classes within a .NET assembly, and Microsoft is making this freely available as part of a Feature Pack for SQL Server (which will be redistributable as part of your stand-alone application, according to Isaac):

SQL Server 2008 RC0 Feature Pack Download Page

(Look for "Microsoft SQL Server System CLR Types," which includes the two Spatial types plus the HierarchyID type.  This link is for RC0, and may not be applicable to future versions as the product is finalized and released.)

Builder API

A new feature that was included with the first Release Candidate (RC0) is the Builder API.  This is a collection of interfaces and classes that helps you to construct spatial types by specifying one point at a time until all points have been added.

The Builder API is not only useful for creating new instances of spatial data, but also for consuming existing instances one point at a time (maybe to convert an instance into another format).  Documentation is light at the moment, so I'm still trying to grok exactly how to best utilize it.

For my first experiment with the API, I obtained some Zip Code Boundary data in ASCII format from the U.S. Census Bureau:

http://www.census.gov/geo/www/cob/z52000.html#ascii

My goal was to parse the data, and then create a new SqlGeography instance for each zip code.  (Note: SqlGeography is the .NET class name that T-SQL refers to simply as Geography).  The SqlGeographyBuilder class proved to be perfect for accomplishing this task.

At its core, the SqlGeographyBuilder implements the IGeographySink interface.  If you wanted to consume an existing SqlGeography instance, you could implement IGeographySink in your own class, and then invoke the SqlGeography's Populate() instance method, passing in your object as the parameter.  The Populate() method takes care of calling the appropriate IGeographySink methods within your class.

In this case, I'm not starting with an existing SqlGeography instance, so my code will need to call the methods of the SqlGeographyBuilder in the correct order:

IGeographySink

After EndGeography() has been invoked, the new instance is available via the ConstructedGeography property of the SqlGeographyBuilder class. 

Simple enough, right?  Yeah, I'm still a little lost myself...  But, here's some code to help demonstrate what's going on!

First, let's look at the ASCII data.  A single zip code's boundary might be defined as:

      1469      -0.824662148292608E+02       0.413848583827499E+02
      -0.824602851767940E+02       0.413864290595145E+02
      -0.824610630000000E+02       0.413860590000000E+02
      -0.824685900000000E+02       0.413841470000000E+02
      -0.824686034536111E+02       0.413843846804627E+02
      -0.824605990000000E+02       0.413863160000000E+02
      -0.824602851767940E+02       0.413864290595145E+02
END

 

The very first line happens to contain an identifier (maps to a second file that lists the actual USPS zip code).  The coordinate listed in the first line is not actually part of the boundary, but rather appears to be the population center of that area.  The actual boundary begins with the second line, and continues until you encounter the "END".  Also, in case you couldn't tell, coordinates in this data are in Longitude-Latitude order.

Since a Zip Code is a polygon, and since we are working with SqlGeography, we must be aware of ring ordering.  That is, the exterior ring of a polygon must be defined in a counter-clockwise order so that as you "walk the ring", the interior is always to your left.  If you reverse the order, then SqlGeography assumes that you're trying to define a polygon containing the entire world except for the small area inside of the polygon.

Well, in this case, the order of the points of the Zip Code boundary is defined in clockwise order... so, we must be aware of this and call into the SqlGeographyBuilder in the opposite order (so the last point defined in the ASCII data is the first point used while building our new instance). 

To accomplish this, I simply parse the Lat/Long coordinates as "double" types, and then push them onto a stack.  Then, I pop the stack and call into the Builder API with each point.  At the end, I obtain the new SqlGeography instance from the ConstructedGeography property. 

(Note: This is demonstrative code - some things should probably be cleaned up/refactored/error handled... You have been warned)

public SqlGeography ParseAsGeography(string zipcode_points)
{
    StringReader sr = new StringReader(zipcode_points);
    string line = sr.ReadLine();

    Stack<double[]> Points = new Stack<double[]>();

    while (line != null  && line != "END")
    {
        if (line != String.Empty)
        {
            Points.Push(ParseLatLngValues(line));
        }

        line = sr.ReadLine();
    }

    return CreateGeography(Points);
}

private double[] ParseLatLngValues(string line)
{
    //      -0.838170700000000E+02       0.409367390000000E+02

    double[] ret = new double[2];

    string lng = System.Text.RegularExpressions.Regex
.Matches(line, "\\S+")[0].Value; string lat = System.Text.RegularExpressions.Regex
.Matches(line, "\\S+")[1].Value; double.TryParse(lat, out ret[0]); double.TryParse(lng, out ret[1]); return ret; }

private SqlGeography CreateGeography(Stack<double[]> points)
{
    SqlGeographyBuilder builder = new SqlGeographyBuilder();
    builder.SetSrid(4326);
    builder.BeginGeography(OpenGisGeographyType.Polygon);

    double[] point = points.Pop();

    builder.BeginFigure(point[0], point[1]);

    while (points.Count > 0)
    {
        point = points.Pop();
        builder.AddLine(point[0], point[1]);
    }

    builder.EndFigure();
    builder.EndGeography();

    return builder.ConstructedGeography;
}

SQL Server 2008: Spatial Data, Part 1

SQL Server 2008: Spatial Data, Part 2

SQL Server 2008: Spatial Data, Part 3

SQL Server 2008: Spatial Data, Part 4

SQL Server 2008: Spatial Data, Part 5

SQL Server 2008: Spatial Data, Part 6

SQL Server 2008: Spatial Data, Part 7

kick it on DotNetKicks.com

Print | posted on Monday, June 23, 2008 10:18 PM | Filed Under [ Articles .NET SQL Spatial ]

Feedback

Gravatar

# re: SQL Server 2008: Spatial Data

Very good article about the spatial functions in the SQL server 2008.

Keep it up.
7/10/2008 3:32 PM | Mani
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Your blogs helped me a lot to enhance my SQL knowledge.

The only thing I felt that few more additional data types examples should be included so as to clear its practical concepts.

KEEP SMILING.
7/18/2008 6:46 AM | Athar Shaikh
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Can instances of SqlGeography be created outside a SQL Server 2008 instance and subsequently persisted in a SQL Server 2008 instance? In reverse, can instances of SqlGeography be retrieved directly from a SQL Server instance? I.e. can I use the SqlGeography class as my value object when working with SQL Server 2008 instead of WKT and WKB?
8/5/2008 9:55 PM | mIKE
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

A colleague and I tried an interesting experiment involving STDistance. I created a table with 100,000 random points within a given constraint (roughly the size of Colorado), using lat and long as decimal columns and then a geographic point comprised of the lat and long. Then I picked a point in roughly the center of the group. Using STDistance in the select statement to return all points within 5km was significantly slower than using a User Defined function where we did the math manually. Is that behavior to be expected?
8/13/2008 7:02 PM | Ryan Smith
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

@ mIKE

Answer to all your questions is yes! Every UserDefinedType (including SqlGeometry and SqlGeography) has Read and Write methods which can be used to convert it to/from varbinary type (binary format used is different from WellKnownBinary).
So when you select your geography column in query, server will actualy return varbinary which can be converted to SqlGeography using Read method.

@Ryan Smith

Geography type models Earth as an elipsoid, so SqlGeography.STDistance computes minimal distance along an eliptical arc which can only be computed numericaly, which is somewhat slow. If you don't need accurate results you can model Earth with a sphere and use direct (and faster to compute) formula.

... but, performance of queries like "SELECT ... WHERE shapeGeog.STDistance(@queryPoint) < @distance" can be significantly improved by creating a spatial index on shapeGeog column.
9/27/2008 3:11 PM | Marko Tintor
Gravatar

# converting exiting columns to shapes

This is really good stuff, thanks.

Say I have a table with 8 columns: x1,y1,x2,y2,x3,y3,x4,y4.
I add a ninth column 'poly' as geography.
Is there a SQL statement that will take the values for each coord, and store in 'poly' for each row?
Could just as easily copy to another table if that is easier.
10/14/2008 9:53 PM | p-dop
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

My first thought is that you should be able to use simple string concatenation to construct WKT used to instantiate the geography type as part of the UPDATE statement. Perhaps something like the following (untested):

UPDATE tbl
SET poly = geography::STGeomFromText('POLYGON(('
+ CAST(x1 AS varchar) + ' ' + CAST(y1 AS varchar) + ', '
+ CAST(x2 AS varchar) + ' ' + CAST(x2 AS varchar) + ', '
+ CAST(x3 AS varchar) + ' ' + CAST(x3 AS varchar) + ', '
+ CAST(x1 AS varchar) + ' ' + CAST(x1 AS varchar) + '))', 4326)

This is assuming that your "x" coordinates are longitude and "y" coordinates are latitude. Also note that Polygons are closed shapes, so the last POINT in the construction is the same as the first point.

Ring ordering might be a gotcha for you - if your coordinates are defined in reverse order (i.e. clockwise instead of the expected counter-clockwise), then you'll want to reverse the order that you construct the WKT (i.e., start with x3,y3 and work down to x1,y1). Admitedly, this is the most difficult part for me to recognize when I first come across a set of points, so I usually wait for SQL Server to give me an error about shapes being larger than a hemisphere to know that I have the ring ordering reversed.
10/15/2008 7:46 AM | jfollas
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Very cool, thanks. This worked great.

I have three tables of rectangles: one clockwise, one counter, and mixed.
The first two worked after correcting the statement appropriately for one, but the third failed either way.
I guess it fails completely if even one polygon in the entire column is malformed. I had hoped that the correctly oriented ones would have been updated. Then i could reverse the orientation in the query and run again but...no luck.
No big, i now have two big tables of polygons as geography types thanks to you!

Now i'm trying to select rows that intersect a polygon but not having much luck.
Latest attempt:
DECLARE @g as geography;
SET @g = geography::STGeomFromText('POLYGON((-122.4 37.8, -122.2 37.8, -122.2 37.9, -122.4 37.9, -122.4 37.8))', 4326);
SELECT poly.STAsText() from Image_Footprint
WHERE @g.STIntersects(poly)
10/16/2008 1:22 PM | p-dop
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Answered my own question, changed WHERE:
WHERE @g.STIntersects(poly)=1
10/16/2008 2:35 PM | p-dop
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Answer, change WHERE:
WHERE @g.STIntersects(foot)=1
10/16/2008 2:40 PM | p-dop
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Hello Jason,
That's a very useful series of articles on Spatial Data with SQL2008.
Thanks a lot.
10/21/2008 6:17 AM | Eralper
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Great info here. Thanks for sharing!!

11/20/2008 3:56 PM | dave
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Is there Java support for the SQL Server spatial datatypes? For example, a Java class like 'java.sql.Geometry' so from a Java program you can query a SQL Server table with a spatial column(s) and iterate over the result set using such a class.

Thanks,
Colm
3/19/2009 7:29 AM | Colm McHugh
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

I'm just guessing here, but providing native Java support for any of the .NET CLR types probably isn't in Microsoft's list of things to do.
3/19/2009 7:48 AM | Jason
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Hi,
I have a geometry of type SqlGeometry and then I convert it into varbinary.
I then use a BinaryReader to read the bytes.
BinaryReader r = new BinaryReader(new MemoryStream(byteGeomIn));
Then I use SqlGeometry's read method to read the binary.
sqlGeom.Read(r);
It should be working properly,but I am getting an error at the Read statement. It says Invalid Format or Spatial Reference Id should be between 0-9999. But while creating the geometry type in Sql, i have given the SRID as 0. I dont understand what the problem could be.
6/17/2009 7:08 AM | Swat
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

My first guess is that converting to varbinary is not resulting in the same bytes (i.e., perhaps varbinary wraps its content with meta data, such as a length indicator?).

Is it even necessary to convert to a varbinary? If you select a Geometry column as part of a query, you'll get a binary serialized version that can then be used by .NET code on the receiving side - no casting required.
6/17/2009 9:19 AM | Jason
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

I have 2 questions here
1. I need to convert my geometry object into a geometry blob. How do I do that in the way you suggested then?
2. I have a query which retrieves the geometry column. Say for eg.
select geom from mytable;[geom is my column with the geometry datatype]. The result window shows me something in the form of a binary. However when I say select geom.STAsBinary from mytable, the result window has some values which are slightly different from my previous query. Which is the binary representation that my .Net code actually takes??
6/18/2009 3:16 AM | Swat
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

When you do a query like:

SELECT GeomColumn FROM TABLE

The binary serialized version of the Geometry will be returned (you can see this in Management Studio's result pane). This is the version that the Read() method uses, and is produced by the object's Write() function.

There's a discussion about what these bytes represent here:

social.msdn.microsoft.com/.../40ee9466-d7bf-434...

The STAsBinary() function, on the other hand, produces Well-Known Binary as its output, which can be shared across multiple platforms that understand WKB (i.e., Oracle, DB2, ESRI, etc). However, these bytes are not compatible with the binary serialized version that Read() expects.

Instantiating a SqlGeometry from .NET code will be expecting the bytes from the first binary form (i.e., just SELECT the column).
6/18/2009 7:18 AM | Jason
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

Thanks a lot for clarifying the difference btwn the 2 of them. I kinda managed to find out some solution.
I converted my geometry object into a varbinary and then passed it to my method as a byte array. Then used the binary reader to read the bytes. Finally the SqlGeometry's read method to read the binary.

But I still am not convinced as to why the reader cant directly take in the binary format of my SqlGeometry.
6/18/2009 9:04 AM | Swat
Gravatar

# re: SQL Server 2008: Spatial Data, Part 8

It should. Forgive my laziness here, but I'm not in a spot to code up an example that I can test and ensure that it works... But, you should be able to directly cast from a SqlDataReader's field to a SqlGeometry.

Something like this:

// SELECT geomcol FROM table

SqlDataReader rdr = sqlcmd.ExecuteReader();
while (rdr.Read())
{
    SqlGeometry geom = (SqlGeometry) rdr[0];
}

(See this article on accessing SQLCLR UDTs from code: msdn.microsoft.com/en-us/library/ms131080.aspx )

6/18/2009 7:37 PM | Jason
Comments have been closed on this topic.

Powered by: