Getting an Attribute value for a specific Geometry

Topics: Data Access, General Topics, SharpMap v2.0
Jan 29, 2009 at 8:26 PM
This is my first post.  I would like to state that SharpMap is Great!  Even though the learning curve has been steep for me.(I am learning C# as I go) 

I am doing a web project in that shapefiles are drawn and queried upon a virtual earth map.  I am also providing tools to work with the data.  I do have some experience with ArcObjects and VB.. but stuff is done much differently in SharpMap.  I am posting this to help other folks because I found a solution and to solicite from the experts a better or more efficient way of accomplishing this task.

I am using Juan DoNeblo template for using Sharpmap to read a shapefile and display the drawing on a Virtual Earth Map through a web service call and javascript.  My client wanted to color code lines from a shapefile based upon a data attribute field.  In arcobjects this easy because the feature is linked through the FID.  I could not figure out how to access the FID through the geometry.  Basically I am using Sharpmap to read a shapefile and convert all the vertices to points and send them via webservice back to the client from which I use the VE API to draw if points or polygons lines between vertices.  So when I make my call to the web service I am sending the path to the shapefile and the fieldname to  which to attach the value for each feature sent back.  My first attempt using the Intersections methods would result in set of multiple features .  I had no way of telling which record went with what geometry (did not know how to access the FID through the Geometry collection.)  Looking at the files I noticed that the FeatureDataRow had a geometry property.  So I reconfigured  and looped through the BoundBox results for Geometries that were equal.  If they were I then queried attibute value and sent that along with a point collect (list) so that VE could draw the shape. 

Is there a more efficient or better way of doing this? --- I would feel much better equating FID's or Object ID as in the ESRI framework.  

<code snippet of web service>
 if (sf.ShapeType != ShapeType.Polygon && sf.ShapeType != ShapeType.PolyLine &&
     sf.ShapeType !=
ShapeType.MultiPoint && sf.ShapeType != ShapeType.Point)
     return null;
   // Initializing the list of Shapes to be returned
   List<Shape> layer = new List<Shape>();
   // Retrieving all the geometries in the Shapefile
   BoundingBox ext = sf.GetExtents();
   IEnumerable<Geometry> geometries = sf.ExecuteGeometryIntersectionQuery(ext);
   // Iterating through all the geometries in the shapefile, converting them
   // to Shape objects and adding them to the output list
   // along with the value of the attribute field (further classify the features)
     
foreach (Geometry geometry in geometries){
        
List<PointF> points = new List<PointF>();
        
string gClass = "";
       
// Eval only if a Field name is passed.
       
if (classify.Length > 0){
            FeatureDataSet ds = new FeatureDataSet();
            // fill a dataset with those items within the individual geometry
            // bounding box
            sf.ExecuteIntersectionQuery(geometry.GetBoundingBox(), ds);
             
FeatureDataTable dt = ds.Tables[0];
          
// Check to see if the intersection results in multiple records
          
// ideally should be one but geometry shape can be such that multiple
          
// other parts will fall in the bounding box.
         
if (dt.Rows.Count > 0){
            
// Check to see if the attribute data features geometry matches
            
// the Geometry which we are loop through. If so then
            
// assign the value of the field to the returning variable
           
// this way I am sure the right data is being assign to the correct
            
// geometry
                
foreach (FeatureDataRow row in dt.Rows){
                    
if (geometry.Equals(row.Geometry))
                        gClass = row[classify].ToString();
                    }
         }
}
// For Polygons
if (sf.ShapeType == ShapeType.Polygon)
......
Coordinator
Jan 29, 2009 at 10:35 PM
Edited Jan 29, 2009 at 10:41 PM
Hi tkowal, just to clarify a couple of things - the v2 branch here on codeplex is very old.. we tried to bring the 'live' v2 code back to codeplex but had issues with the svn bridge.. the real v2 is at http://sharpmapv2.googlecode.com/ . With this in mind you may be better using the trunk (0.9) from codeplex, or if you are brave the real v2 - warning though - there will be a few substantial changes in the near future..

Back to the matter at hand..
I am not quite sure of the problem.. You seem to be retrieving all Geometries in the shapefile then intersecting each one with all the other geometries (at least their bounding boxes anyway) in order to get the attribute value for the test geometry. Is this what you intend? Or do you just want to get each geometry and its associated attribute value and use these to populate a list of shape objects?

If the latter is the case the following may help (note: it was written in notepad and hasn't been anywhere near a compiler so it may not work out of the box).. cheers jd

if(string.IsNullOrEmpty(classify))
{
 return null; //do no further processing as we dont have an attribute to test.
}

 

if (sf.ShapeType != ShapeType.Polygon 
    && sf.ShapeType != ShapeType.PolyLine 
    && sf.ShapeType != ShapeType.MultiPoint 
    && sf.ShapeType != ShapeType.Point)
     return null;
// Initializing the list of Shapes to be returned
List<Shape> layer = new List<Shape>();
// Retrieving all the geometries in the Shapefile

BoundingBox ext = sf.GetExtents();
FeatureDataSet ds = new FeatureDataSet();
sf.ExecuteIntersectionQuery(ext, ds);
FeatureDataTable<uint> dt = ds.Tables[0] as FeatureDataTable<uint>;//this table should have a row for each record in the shapefile
foreach (FeatureDataRow<uint> row in dt.Rows)
{
     string gClass = row[classify].ToString(); //find the desired attribute for the row
     Geometry geom = row.Geometry;
     uint featureId = row.Id;

     if (sf.ShapeType == ShapeType.Polygon)
     {
          List<PointF> points = new List<PointF>();
          //TODO:read the geometries' coords into the points list
          //TODO:create your shape object
          //TODO:add your shape object to the 'layer' list

     }
}


Jan 30, 2009 at 2:27 PM
John,

Thanks for the quick reply!  I tried using the .9 stable version but could not get my webservice to work with it.  It is working with the v2 version I got from Juan DoNeblo -- I tried the version 2 on codeplex but that too did work for me -- your reply solved the mystery .  I am struggling to learn C -- my background is old vb3 through vb5  when I did program.  Thanks for the c help too! 

To put in context what I am doing.. The results or list of shape objects are sent back to my web application and converted into something the Virtural Earth API can render on screen.  So all my symbology and rendering has to occur outside of sharpmap.  I am using various attribute fields to provide the logic of which symbols to render.

To answer your question,  "...to get attribute value for the test geo or get each geometry and associated attributes" -- eventually both.  But for matter posted it was to get each geometry and associated attributes (Optionally).  The classify field is optional.  If it is blank, I only need the geometry list -- If a fieldname is passed then return the geometry list and the associated value of the field.

Your suggetion is much more efficient to that what I posted!  I will try it out.  Out of ignorance I was perfoming an unecessary test, it did not dawn on me to simply form the inital collection as a FeatureDataSet and loop through each row and simply use the row geometry. -- COOL -- I am glad I posted to the forum!

Next I will be trying to do the reverse -- Forming a Filtered List of Geometries based upon an Attribute value.  I believe you inspired me and set me on the right path to accomplish the second task.  

Thanks for your input!

Ted
Coordinator
Jan 30, 2009 at 2:40 PM
Hi Ted, for your second filtered one check out the FilterDelegate property on the shapefile provider. You can use this to attach a method which is called when populating the FeatureDataTable so that you only have matching results. hth jd
Jan 30, 2009 at 7:00 PM
John,

Worked like a champ!

Being a VBer Delegates are hard to wrap my head around! 

Looking at FilterDelegate it appears to do what I want, but is this static?  I would be using this as a web service and I would not know what field or what value the filter would be set on beforehand.

Example
One use may want to extract geometries from say the Project Layer that meet this condition:
         Projects that are "Under Construction"  - STATUS = "Under Construction"
Another may want
        Projects that were built in 2008  = YR_BUILT=2008

I would rather not write a filter for every possible combination...

I was looking to create one generic filter method that a shapefile, field name, test (=,<>,>,<, or like) and limiting value could be sent to a method to perform the filtering.  Well that is my general thought until reality bites  -- at this point it is only conceptual so everything is doable !   :)

Coordinator
Jan 30, 2009 at 7:12 PM
The property is not static. It is of a delegate type which means that any method with the same signature can be used either an existing member on a type or a new method declared inline in the scope of the web service method. If it is declared inline  you can access to local parmeters in the declaring method.. So you can use the field name passed in within your delegate.. I will try and do a little demo when I get a chance but it is likely to be next week.. check out anonymous delegates and lambda expressions on http://msdn.microsoft.com 

If  you want to go really deep you can have delegates that write new delegates!..

Anyway.. good luck jd 
Jan 30, 2009 at 7:17 PM
That sounds real promising ...  I am in no real hurry.  Gives me time to learn about delagates!

I have lots of tedious stuff to do on the map in the meantime like creating Legends and stuff and Help arrrgh!

Thanks for all of your help -- have a great weekend.

Ted
Coordinator
Feb 2, 2009 at 6:00 PM
Edited Feb 2, 2009 at 6:09 PM
Hi Ted, here we go.. start with the basic delegates

            ShapeFile sf = new ShapeFile(path);

//method 1 use an inline anonymous method
            sf.FilterDelegate = delegate(FeatureDataRow r)
                                    {
                                        return (int)r["Population"] > 999;
                                    };

//method 2 (vs2008 onwards) use an inline lambda expression
            sf.FilterDelegate = r => (int)r["Population"] > 999;

//method 3 use an existing member see the implementation below
            sf.FilterDelegate = new ShapeFile.FilterMethod(MyFilterMethod);
//which can be abbreviated to
            sf.FilterDelegate = MyFilterMethod;

        private static bool MyFilterMethod(FeatureDataRow dr)
        {
            return (int) dr["Population"] > 999;
        }

Coordinator
Feb 2, 2009 at 6:03 PM
Now we can use local variables like so:

            ShapeFile sf = new ShapeFile("path.shp");

            string column = "Population"; //this can be a param passed into your webservice
            int min = 999;//and so can this

   //method 1 again
            sf.FilterDelegate = delegate(FeatureDataRow r)
                                    {
                                        return (int)r[column] > min;
                                    };

//and method 2
            sf.FilterDelegate = r => (int)r[column] > min;

hth jd
Feb 3, 2009 at 1:43 PM
Thanks John!  You have been a real help getting me over the hump!

I will post back my success and failures .... in case this can be useful for anyone else reading this thread.  The web app is coming along very well.  It has the feeling of being very "Solid"!

I just wish more was posted on customizing and using the VE Api -- and that VE had a concept of Annotation!

Again thanks for the help!

Ted
Feb 3, 2009 at 7:50 PM
John,

I tried to follow your template but cannot get it to work.  First in using the lambda method (int)r....  compiler complained that it was expecting a bool type. So I made appropriate changes.  But when I use the ExecuteIntersetionQuery (to best of my poking around uses the filterdelegate)  the webservice bombs with a nullexception error --- not very helpful. 

Sort of stuck!



    public List<Shape> LoadShapefileLayer(string filePath, string classify, string filterColumn, string filterValue, string cmd)
    {
        // SharpMap's Shapefile reader / writer
        ShapeFileProvider sf = null;
       
        try
        {
            sf = new ShapeFileProvider(filePath);
            sf.Open(false);
           
            if (sf.ShapeType != ShapeType.Polygon && sf.ShapeType != ShapeType.PolyLine
                && sf.ShapeType != ShapeType.MultiPoint && sf.ShapeType != ShapeType.Point)
                return null;
                //Todo: add logic to pre filter the provider
                if (!string.IsNullOrEmpty(filterColumn))
                {
                   switch (cmd)
                   {
                      case "EQUALS":
                         sf.FilterDelegate = r => (bool)r[filterColumn].ToString().ToLower() == filterValue.ToLower());
                         break;
                      case "CONTAINS":
                         sf.FilterDelegate = r => (bool)r[filterColumn].ToString().Contains(filterValue);
                         break;
                   }
                }
                // Initializing the list of Shapes to be returned
                List<Shape> layer = new List<Shape>();
                // Retrieving all the geometries in the Shapefile
                BoundingBox ext = sf.GetExtents();
                // Initialize a Feature Data Set
                FeatureDataSet ds = new FeatureDataSet();
                // Grab all of the Geometries and Data for the Shape
                sf.ExecuteIntersectionQuery(ext, ds);
    //^^^^^^   GET a NULL EXCEPTION ERROR HERE ON The LINE ABOVE^^^^^^

                //The folowing table should have one row for each record in the shapefile
                FeatureDataTable<uint> dt = ds.Tables[0] as FeatureDataTable<uint>;
                // Iterate through all the rows in the shapefile, assign attribute value if
                // necessary and build export list.
                foreach (FeatureDataRow<uint> row in dt.Rows)
                {
                   string gClass = "";
                   // Assign the classify field  if fieldname is sent
                   if(!string.IsNullOrEmpty(classify))
                   {
                      gClass = row[classify].ToString();
                   }
       ...  // goes on to assign the geometry vertices to a pointF list which is sent back to the VE api for rendering

Coordinator
Feb 3, 2009 at 9:36 PM

Hi again Ted attached is some code for the trunk/0.9 release.. it has a few changes from the code you posted which sems to be for a really old version of v2 but I dont have access to it..
I may have previously misinformed you slightly with FeatureDataTable<uint> above, (which is current v2 code) this is rectified below.: hth jd

using System;
using System.Collections.Generic;
using SharpMap.Data;
using SharpMap.Data.Providers;
using SharpMap.Geometries;

namespace Demo
{
    public class Demo
    {
        //make sure we always use the correct spelling of the command names (ideally we would use an enum for this purpose instead)
        public const string COMMAND_EQUALS = "EQUALS";
        public const string COMMAND_CONTAINS = "CONTAINS";

        public List<Shape> LoadShapefileLayer(string filePath, string classify, string filterColumn, string filterValue,
                                              string cmd)
        {
            // SharpMap's Shapefile reader / writer
            ShapeFile sf = null;

            sf = new ShapeFile(filePath);
            sf.Open();

            if (sf.ShapeType != ShapeType.Polygon && sf.ShapeType != ShapeType.PolyLine
                && sf.ShapeType != ShapeType.Multipoint && sf.ShapeType != ShapeType.Point)
                return null;
            //Todo: add logic to pre filter the provider
            if (!string.IsNullOrEmpty(filterColumn))
            {
                switch (cmd)
                {
                    case COMMAND_EQUALS:
                        sf.FilterDelegate =
                            r =>
                            string.Compare(r[filterColumn].ToString(), filterValue,
                                           StringComparison.CurrentCultureIgnoreCase) == 0
                            ;
                        break;
                    case COMMAND_CONTAINS:
                        sf.FilterDelegate = r => r[filterColumn].ToString().Contains(filterValue);
                        break;
                }
            }
            // Initializing the list of Shapes to be returned
            var layer = new List<Shape>();
            // Retrieving all the geometries in the Shapefile
            BoundingBox ext = sf.GetExtents();
            // Initialize a Feature Data Set
            var ds = new FeatureDataSet();
            // Grab all of the Geometries and Data for the Shape
            sf.ExecuteIntersectionQuery(ext, ds);

            //The folowing table should have one row for each record in the shapefile
            FeatureDataTable dt = ds.Tables[0];
            // Iterate through all the rows in the shapefile, assign attribute value if
            // necessary and build export list.
            foreach (FeatureDataRow row in dt.Rows)
            {
                string gClass = "";
                // Assign the classify field  if fieldname is sent
                if (!string.IsNullOrEmpty(classify))
                {
                    gClass = row[classify].ToString();
                }
                // goes on to assign the geometry vertices to a pointF list which is sent back to the VE api for rendering
            }

            return layer;
        }
    }
}

========================TEST====================== 

using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Demo.Test
{
    /// <summary>
    /// Summary description for UnitTest1
    /// </summary>
    [TestClass]
    public class UnitTest1
    {
        /// <summary>
        ///Gets or sets the test context which provides
        ///information about and functionality for the current test run.
        ///</summary>
        public TestContext TestContext { get; set; }

        [TestMethod]
        public void TestMethod1()
        {
            var d = new Demo();

            string cmd = Demo.COMMAND_EQUALS;
            string value = "AONB";
            string column = "CLASS";
            string classify = "myattributecolumn";

            List<Shape> shapes = d.LoadShapefileLayer(@"D:\Shapefiles\character_areas.shp", classify, column, value,
                                                      cmd);

            Assert.IsNotNull(shapes);
        }
    }
}

Feb 5, 2009 at 3:44 PM
I tried the .9 version dll at codeplex and got the same System.NullException error ... I also download the current version 2 trunk at google -- I was humbled by the complexity -- quickly got lost on how to compile.  -- my code uses Sharpmap.Geometries and the closest class you have in the trunk is .SimpleGeometries and for the life of me I could not figure out how to compile it.  It references a Geoapi that contains Utilities and contains a w...reader and writer.  The two Geoapi in the branch has one but not other.... oh well looks like I will be waiting for the official 2 release.

The filterdelagate worked as suppose to - it correctly returns true or false but with all the versions and variant I tried when I did an IntersectionQuery to load a Dataset -- It bombed there.

while I am waiting for the offical release.. I resorted to filtering the real old fashion way.... not efficient but my datasets are generally less than 5,000 records..  

Thanks for you help....I have learned allot!  For the rest of  y'all who are following this thread... I am using this technique to filter until I am able to obtain the official version 2.... then I will again try and apply the .FilterDelegate.

  // .....<snip>

                foreach (FeatureDataRow<uint> row in dt.Rows)
                {
                   string gClass = "";
                   // Assign the classify field  if fieldname is sent
                   if(!string.IsNullOrEmpty(classify))
                   {
                      gClass = row[classify].ToString();
                   }
                   //Todo: Change this to a FilterDelegate when SharpMap V2 is released
                   if (!string.IsNullOrEmpty(filterColumn))
                   {
                      switch (cmd)
                      {
                         //Todo: enumerate the Commands types   ie FilterCmd.EQUAL, FilerCmd.CONTAINS, etc...
                         case "EQUALS":   
                            proccessGeo = string.Compare(row[filterColumn].ToString(), filterValue,
                                                 StringComparison.CurrentCultureIgnoreCase) == 0;
                            break;
                         case "CONTAINS":
                            proccessGeo = row[filterColumn].ToString().Contains(filterValue);
                            break;
                      }
                   }
                   else
                   {
                      proccessGeo = false;
                   }

                   if (proccessGeo)
                  {

                           // ..... Process the Geometry if  proccessGeo is true .....  Inefficienct but works.....