ThinkGeo.com    |     Documentation    |     Premium Support

Thematic map from SQL business data

Hi,


I am currently trying to implement a thematic map using business data.  Your examples create a nice thematic map from data in the shape file.  My question is how can I show, for example, how many customers we have in each state and color code the states using ClassBreaks?


Any help would be appreciated!



Jeremy, 



Thanks for evaluating Map Suite Web Edition! Here is the sample showing how to use ClassBreak, please have a look. (the data cntry02.shp can be found under "C:\Program Files\ThinkGeo\Map Suite Web Evaluation Edition 3.0\Samples\CSharp Samples\SampleData\World"). 


   protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                Map1.MapBackground.BackgroundBrush = new GeoSolidBrush(GeoColor.FromHtml("#B3C6D4"));
                Map1.CurrentExtent = new RectangleShape(-140, 60, 140, -60);
                Map1.MapUnit = GeographyUnit.DecimalDegree;

                // Add ClassBreak Styles
                ClassBreakStyle classBreakStyle = new ClassBreakStyle("POP_CNTRY");
                classBreakStyle.ClassBreaks.Add(new ClassBreak(double.MinValue, AreaStyles.Grass1));
                classBreakStyle.ClassBreaks.Add(new ClassBreak(1000000, AreaStyles.Evergreen2));
                classBreakStyle.ClassBreaks.Add(new ClassBreak(10000000, AreaStyles.Evergreen1));
                classBreakStyle.ClassBreaks.Add(new ClassBreak(50000000, AreaStyles.Crop1));
                classBreakStyle.ClassBreaks.Add(new ClassBreak(100000000, AreaStyles.Forest1));

                ShapeFileFeatureLayer worldLayer = new ShapeFileFeatureLayer(MapPath("~/SampleData/world/cntry02.shp"));
                worldLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
                worldLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(classBreakStyle);
                worldLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(new TextStyle("POP_CNTRY", new GeoFont("Arail", 8), new GeoSolidBrush(GeoColor.SimpleColors.Black)));
                Map1.StaticOverlay.Layers.Add("WorldLayer", worldLayer);
            }
        }

Thanks, 



Ben



Ben,


Thanks for the reply, however the problem I'm having is not with ClassBreaks, which  are fairly straight forward.  The problem is that I want to add business data from SQL, namely how many customers are in each state, and "join" that information with data in the shape file to be able to color code each state by the number of customers in that state.


Currently I am attempting to do this by using the following code:



private void GetCounterparties(ShapeFileFeatureLayer worldLayer) 

DataSet counterpartiesDataSet = this.GetCounterparties(); 
worldLayer.FeatureSource.Open(); 
Collection<FeatureSourceColumn> columns = worldLayer.FeatureSource.GetColumns(); 
columns.Add(new FeatureSourceColumn("CounterpartyCount", "int", 12)); 

DataTable dt = worldLayer.ExecuteSqlQuery("SELECT STATE_NAME FROM STATES"); 

if (counterpartiesDataSet != null && counterpartiesDataSet.Tables[0].Rows.Count > 0 && 
dt != null && dt.Rows.Count > 0) 

string filterExpression; 
foreach (DataRow drStateName in dt.Rows) 

DataRow [] foundRows; 
filterExpression = string.Format("StateProvince = '{0}'", drStateName["STATE_NAME"].ToString()); 
foundRows = counterpartiesDataSet.Tables[0].Select(filterExpression); 

worldLayer.ExecuteNonQuery(string.Format("UPDATE STATES SET CounterpartyCount = '{0}' WHERE STATE_NAME = '{1}'", foundRows.Length, drStateName["STATE_NAME"].ToString())); 



worldLayer.FeatureSource.Close(); 



My approach was to set a ClassBreakStyle on field values from SQL instead of values from the shapeFile's predefined columns, therefore I tried to add a column to the shapeFile and add values back into this new column using the ExecuteNonQuery method of the ShapeFileFeatureLayer object.  I could not get this approach to work as yet, and I'm sure I'm missing an easier way to do this.


Thanks in advance,


Jeremy


 



Jeremy, 
  
   There may be an easier way indeed!  I can’t remember off hand if it is on the Layer or FeatureSource, my guess would be the FeatureSource, but there is an event called something like CustomFieldFetch.  This little beauty is just what you need.  They way it is used is that in your class break when you need to tell it the field in the DBF you will want you make up a name that isn’t in the DBF.  For example if your shape files DBF has just the StateName column then you make up a new column name like StatePopulation that doesn’t exsist.  When we go to get the data for that column we look to see if it is in the DBF, if not then we raise the CustomFeildFetch and pass you the field name we need, the Id of the  feature and expect you to return us a value.  In this way it integrates your external data seamlessly.  Sorry I don’t have any sample code but I can whip some up if necessary. 
  
   I am not sure if we pass the feature to you in the event or just the Id.  The Id might make it hard for you to know what start it is your dealing with.  If we pass the Feature then things are a little easier.  If it is then one thing you can so also is on the class break style I think there is a collection called something like RequiredColumns.  You need to add a new entry into that specifying the StateName, if that is your linking column.  This will ensure that the Feature we pass you will have the StateName in the Feature.ColumnData.  If this doesn’t work let me know I think we might have to tweak things. 
  
   One thing to note is that what I would do is in the event I would keep a local cache of all of the states data in a data table or dictionary.  When the event is first fired I would check my cache and if empty then make your big SQL call to the database.  Cache the results.  This is because the event will raise for every record and this can cause lots of SQL queries if you do the query every time the event is raised. 
  
   If you don’t like events, like myself then you might also be able to inherit from the ShapeFileFeatureSource and override the OnCustomFieldFetch.  At this point you can do the same thing as in the event.  I am not sure if this is virtual though, but it just might be. 
  
 If I understand the problem right this might be the solution. 
  
 NOTE:  I think there is a sample app that does this under the Querying Feature Layers and the sample is ‘Add custom data to a feature layer.’ in the How Do I samples, check it out.  If you cant find it look on the web samples online.  You can see the source as well.  I am not sure of the web and desktop samples are exactly the same. 
  
 websamples.thinkgeo.com/webeditionsamples/ 
  
 David

David,


Thanks for the reply.  I have seen that example and after looking at it, it looks like the CustomColumnFetch event passes two parameters, sender and eventArgs.  I don't see the event passing the Feature object, so it is definately more difficult to tell what state I'm dealing with.  Could you post some example on how to do this?


Thanks,


Jeremy



Jeremy,


Here attached is the sample code how to use CustomColumnFech event for ClassBreak Style, please have a look.


Thanks,


Ben

 



322-AddMyOwnCustomDataByCustomColumnFetch.aspx.zip (1.32 KB)

Given the Id, how do I get a different column out of the shape file?

Fergus,


  I knew this might be the next question.  What you can do is get at the sender object which is the FeatureSource for the Layer and then call methods on it get get back values.


David

 




            // The sender object is really a ShapeFileFeatureSource so we need to 
            // cast it as that.
            ShapeFileFeatureSource featureSource = (ShapeFileFeatureSource)sender;
            
            // Next we can call the handy GetDataFromDbf method to get back
            // a column based on the Id
            string countryName = featureSource.GetDataFromDbf(e.Id, "cntry_name");


Got it, thanks

Fergus, 
  
   Let me know how it goes.  Also I think we will consider adding the Feature parameter to the event arguments.  This will save you a step in the future.  Then you will not have to query the DBF.  You could just add the fields you wanted to the style instead and they would be there.  This is the way I think this scenario should play out.  I will keep you posed on this but for now the code I sent is about the same thing. 
  
 David

I was up against the same problem, and came up with an eventless solution by extending ClassBreakStyle to accept external custom data. It seems to be a little faster than using the CustomColumnFetch. I’m using this to load up all census blockgroups at a state level, so performance is a major concern. Let me know if you see any problems or see room for improvement.


[Serializable]
public class CustomDataClassBreakStyle : ClassBreakStyle
{
    Dictionary<string, double> customData;

    public CustomDataClassBreakStyle(string columnName, Dictionary<string, double> customData) :
        base(columnName)
    {
        this.customData = customData;
    }

    protected override void DrawCore(IEnumerable<Feature> features, GeoCanvas canvas, Collection<SimpleCandidate> labelsInThisLayer, Collection<SimpleCandidate> labelsInAllLayers)
    {
        foreach(Feature feature in features)
        {
            string key = feature.ColumnValues[this.ColumnName];
            if (customData.ContainsKey(key))
            {
                double value = customData[key];
                for (int i = this.ClassBreaks.Count - 1; i >= 0; i–)
                {
                    ClassBreak classBreak = this.ClassBreaks[i];
                    if (value > classBreak.Value)
                    {
                        // Call the draw on all of the default styles of the ClassBreak, and also check if there are custom styles.
                        Feature[] tmpFeatures = new Feature[1] { feature };
                        if (classBreak.CustomStyles.Count == 0)
                        {
                            classBreak.DefaultAreaStyle.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                            classBreak.DefaultLineStyle.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                            classBreak.DefaultPointStyle.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                            classBreak.DefaultTextStyle.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                        }
                        else
                        {
                            foreach (Style style in classBreak.CustomStyles)
                            {
                                style.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                            }
                        }
                        break;
                    }
                }
            }
        }
    }

    protected override Collection<string> GetRequiredColumnNamesCore()
    {
        Collection<string> requiredFieldNames = base.GetRequiredColumnNamesCore();
        if (!requiredFieldNames.Contains(this.ColumnName))
        {
            requiredFieldNames.Add(this.ColumnName);
        }
        return requiredFieldNames;
    }
}

Usage as follows:

// dictionary to hold key/count pairs
Dictionary<string, double> countLookup = new Dictionary<string, double>();
// existing field to link key
string linkFieldName = “FIPS_CODE”;

// populate countLookup with the external custom data (SQL in my case)
countLookup.Add(‘fips_code_as_key’, 1234);
// …

// the main call
CustomDataClassBreakStyle breaks = new CustomDataClassBreakStyle(linkFieldName, countLookup);
breaks.RequiredColumnNames.Add(linkFieldName);
// then add classbreaks, and add the object to a CustomStyles collection, 





 


Rob,


Your code is straightforward and easy to understand, I think you can go ahead to use it for most cases. Here are some possible ways to make it faster under some circumstance though, please have a look and maybe they will be suitable for you.


1, The main draw method accepts a Feature Array as the parameter but every array you passed in has only one item, that means if we have 100 features, we will always call the Draw method 100 times. As every draw has some overhead, it will be faster if we can draw many features together at a same time. You know draw 100 features one time is faster than draw 1 feature 100 times.


Here is the code I change a bit trying to draw bunch of features together at one time. It will save some time on drawing part but will take more memory and do some extra work on other part. So please have a test and make sure it works fine for you before using it.



    [Serializable]
    public class CustomDataClassBreakStyle : ClassBreakStyle
    {
        Dictionary<string, double> customData;

        public CustomDataClassBreakStyle(string columnName, Dictionary<string, double> customData) :
            base(columnName)
        {
            this.customData = customData;
        }

        protected override void DrawCore(IEnumerable<Feature> features, GeoCanvas canvas, Collection<SimpleCandidate> labelsInThisLayer, Collection<SimpleCandidate> labelsInAllLayers)
        {
            // Create and Init the Feature Collections for every ClassBreak
            Collection<List<Feature>> ClassBreakFeatures = new Collection<List<Feature>>();
            for (int i = 0; i < ClassBreakFeatures.Count; i++)
            {
                ClassBreakFeatures[i] = new List<Feature>();
            }

            // Fill in the Feature Collections for every ClassBreak
            foreach (Feature feature in features)
            {
                string key = feature.ColumnValues[this.ColumnName];
                if (customData.ContainsKey(key))
                {
                    double value = customData[key];
                    for (int i = this.ClassBreaks.Count - 1; i >= 0; i--)
                    {
                        ClassBreak classBreak = this.ClassBreaks[i];
                        if (value > classBreak.Value)
                        {
                            // Call the draw on all of the default styles of the ClassBreak, and also check if there are custom styles.
                            ClassBreakFeatures[i].Add(feature);
                            break;
                        }
                    }
                }
            }

            // Draw them out
            for (int i = 0; i < this.ClassBreaks.Count; i++)
            {
                DrawAllFeaturesForOneBreak(ClassBreakFeatures[i].ToArray(), this.ClassBreaks[i], canvas, labelsInThisLayer, labelsInAllLayers);
            }
        }

        private void DrawAllFeaturesForOneBreak(Feature[] features, ClassBreak classBreak, GeoCanvas canvas, Collection<SimpleCandidate> labelsInThisLayer, Collection<SimpleCandidate> labelsInAllLayers)
        {
            if (classBreak.CustomStyles.Count == 0)
            {
                classBreak.DefaultAreaStyle.Draw(features, canvas, labelsInThisLayer, labelsInAllLayers);
                classBreak.DefaultLineStyle.Draw(features, canvas, labelsInThisLayer, labelsInAllLayers);
                classBreak.DefaultPointStyle.Draw(features, canvas, labelsInThisLayer, labelsInAllLayers);
                classBreak.DefaultTextStyle.Draw(features, canvas, labelsInThisLayer, labelsInAllLayers);
            }
            else
            {
                foreach (Style style in classBreak.CustomStyles)
                {
                    style.Draw(features, canvas, labelsInThisLayer, labelsInAllLayers);
                }
            }
        }
    }

NOTE:


With the new method, the result is a bit different. In your original way, as the records will be drawn one by one, if 2 features are overlap, the latter one will be over the former one. So for example I have 2 classbreaks for the features, one is rendered in Green and the other is in Blue, chances is that in your map, some blue feature is over the green one and some green feature is over the blue one.  In the new way though, as one group of features is rendered together, the green ones will always be over the green ones (or the other way), that might be the way we prefer.


2, If you have many class breaks, instead of looping them one by one, maybe you can sort them first and do a binary search, which will greatly improve the speed. Sure if you only have 2 or 3 classbreaks, that's not a big deal.


Hope that helps, let me know if you have any issues.


Thanks,


Ben




Ben, 
  
 Thanks for the tips and the code. My features don’t overlap, so I don’t have to worry about the order of drawing them. I have 7 classbreaks, so it is probably worth it to do the binary search.  
  
 I’m loving the extensibility! 
  
 -Rob

EDIT: I bet there would be an interest in a place for users to post and share extended classes. Of course, it would also be nice to see some of the real classes as samples. This way we could see what kind of of performance boosting techniques we should use if we override (especially related to Styles, Layers, and FeatureSources).

Rob, 
  
 I totally agree. We plan to have another forum in which user can not only share the codes, but also make other users very easy to contribute to it, like a ThinkGeo PlugIn SourceForge.  Hope we can have it soon:) 
  
 Thanks, 
  
 Ben

Rob, 
  
   As Ben mentioned we are working on a source forge type site where users can share their stuff and we can post code to as well.  Until that gets launched the best place to check is the Developers Blog forum.  Many times when we create a cool sample for a post we will do a little writeup and post it there.  It also has videos and other stuff, highly recommended you check it out.  We post new thing there weekly. 
  
 David