ThinkGeo.com    |     Documentation    |     Premium Support

ValueStyle based on multiple columns

Hi Guys,


I see and understand the ValueStyle based on a single column, how does one get a ValueStyle based on multiple columns?


SHP layer has 3 columns, LN_RTID, LN_OW and LN_GC, how would one set a style for LN_RTID < 950000 and LN_OW = 0 and LN_GC = 1 ?


Regards


John



John,


  There are a few different ways to get the effect you want.  The first is to create your own custom style, it is ironic because we just sent out some details on a webinar to cover this.  Below is some sample code on just how to do this, in the webinar we go into more detail on what is going on but I think you will see the main thrust of the code.


  On thing to note is that in the sample I provided I was not able to test it so there may be a little bug here ot there but it isn't allot of code.  Also I have no idea what these columns mean so for my example I just used the names you provided.  If you want a much better version I would suggest you allow the user to set the column names maybe and give them human readable names.  Since this is really specific to your case hard coding field names etc might not be that bad.  What I hope you get out of this is how you can easily create your own Style classes based on complex logic.  You can create a whole army of these kinds of Style classes tailored to your application.


  On thing this makes me thing of though is that it would be handy to have an ExpressionStyle where the syntax looked something like “[LN_RTID] < 950000 && [LN_OW] == 0 && [LN_GC] == 1”.  At runtime we would replace the column names with the real values and then pass the string to .net to execute it real-time and give us the answer.  I will look into this today and see what I can dig up, this might be easy to implement and very powerful because then you could use logical AND, OR etc.  I might be a bit slow to do but let me check into it.  Thank you very much for sparking this idea!!


  The other way is to use the styles we already have.  The styles are composable meaning you can stack them inside of each other.  For example you have use a ClassBreakStyle to handle the > 950,000 part and then for the style to draw inside of that class break you can place a ValueStyle to handle the other two parts.  One other little trick is that in the system when you specify a field name that name can be composed of multiple fields.  You can use the following syntax: “field1”, this gives you one field or you can use “[field1][field2]” and when we get the values we will parse out things inside the brackets and bet both fields and combine them.  You can also do things like this “[SchoolName] is a great school in [SchoolCityName].” so if you have SchoolName and SchoolCityName as columns you might get a string returned like “Oak Hill is a great school in Sydney”.  You can use this for labeling, styles or just about anywhere else that we take a field name as a parameter.  If you find a place this isn't true then it is a bug on our part.


David


 






using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ThinkGeo.MapSuite.Core;

namespace CustomStyles
{
    [Serializable]
    class JohnCustomStyle : Style
    {        
        private Style displayStyle;
        private int rtId;
        private int ow;
        private int gc;

        public JohnCustomStyle()
            : this(0, 0, 0, new PointStyle())
        { }

        public JohnCustomStyle(int rtId, int ow, int gc, Style displayStyle)
        {
            this.rtId = rtId;
            this.ow = ow;
            this.gc = gc;
            this.displayStyle = displayStyle;
        }

        // This is the style you want to use if everything is correct
        public Style DisplayStyle
        {
            get { return displayStyle; }
            set { displayStyle = value; }
        }

        // Here you can put in your value for RTID
        public int RtId
        {
            get { return rtId; }
            set { rtId = value; }
        }

        // Here you can put in your value for OW
        public int Ow
        {
            get { return ow; }
            set { ow = value; }
        }

        // Here you can put in your value for GC
        public int Gc
        {
            get { return gc; }
            set { gc = value; }
        }

        //  This is where the main logic is and there is not much. Loop through
        // all the features and grab the column values you need.  Check to see if
        // the values match up with the values you have in the properties
        // If they do then draw them using the DisplayStyle.
        protected override void DrawCore(IEnumerable<Feature> features, GeoCanvas canvas, Collection<SimpleCandidate> labelsInThisLayer, Collection<SimpleCandidate> labelsInAllLayers)
        {
            foreach (Feature feature in features)
            {
                int lnRtId = Convert.ToInt32(feature.ColumnValues["LN_RTID"]);
                int lnOw = Convert.ToInt32(feature.ColumnValues["LN_OW"]);
                int lnGc = Convert.ToInt32(feature.ColumnValues["LN_GC"]);
                if (lnRtId < rtId && lnOw == ow && lnGc == gc)
                {                    
                    displayStyle.Draw(new Collection<Feature>() { feature }, canvas, labelsInThisLayer, labelsInAllLayers);
                }
            }
        }

        //  This is a little tricky but really straight forward once you understand it.
        // Here we get the columsn required from the DisplayStlye, if any.
        // Next we add the three column names you need.  This method is to 
        // make sure the fields you require are present when you get the feature
        // in the DrawCore.  We only return column values that are required
        // for performance.  If you do not specify the fields you need then they will
        // not be on the feauture and you will get an exception.
        protected override Collection<string> GetRequiredColumnNamesCore()
        {
            Collection<string> columns = new Collection<string>();

            Collection<string> displayStyleColumns = displayStyle.GetRequiredColumnNames();
            foreach (string column in displayStyleColumns)
            {
                if (!columns.Contains(column))
                {
                    columns.Add(column);
                }
            }

            if (!columns.Contains("LN_RTID"))
                columns.Add("LN_RTID");

            if (!columns.Contains("LN_OW"))
                columns.Add("LN_OW");

            if (!columns.Contains("LN_GC"))
                columns.Add("LN_GC");

            return columns;
        }

        //  This just passes the DrawSample request to the display style.
        protected override void DrawSampleCore(GeoCanvas canvas)
        {
            displayStyle.DrawSample(canvas);
        }
    }
}



Hi David,


Thanks for the code, that works out of the box and does what I want it to do, a little slow but it gets there.


The idea of a ExpressionStyle is where I would like to see this go, that gets powerful fairly quickly and has almost infinite possibilities.


In our current system we have a layer with a bunch of lines on it and various line styles for colours and sold/dash defined using expressions like:-


LN_RTID < 950000 AND LN_OW = 0



LN_RTID < 950000 AND LN_OW = 1


LN_RTID > 950000 AND LN_OW = 0 AND LN_GC = 0


LN_RTID > 950000 AND LN_OW = 0 AND LN_GC = 1


LN_RTID > 950000 AND LN_OW = 1 AND LN_GC = 0


LN_RTID > 950000 AND LN_OW = 1 AND LN_GC = 1


Regards


John


 




John,


  Great news.  I went and did the research I mentioned and found a good way to do the expressions.  I am going to post it soon with a sample etc to the developers blog forum but you can check it out ahead of time.  I whipped this up in a few hours and it is pretty cool. 


A few things..  You need to get the binaries and reference the Flee assembly from the website below.  It is LGPL so as long as you just reference it you can use it in commercial applications.


Homepage

codeplex.com/Flee




CodeProject Article (Great Reference)

codeproject.com/KB/recipes/Flee.aspx


Below is the code and a sample of the usage.  You have to make sure you use the ToInt32 etc as all of the fields from the FeatureSource come out as strings.


David


 





            // Highlight the countries that are land locked and have a population greater than 10 million
            string expression = "(ToInt32(POP_CNTRY)>10000000) AND (ToChar(LANDLOCKED)='Y')";
            FleeBooleanStyle landLockedCountryStyle = new FleeBooleanStyle(expression);

            // You can access the static methods on these types.  We use this
            // to access the Convert.Toxxx methods to convert variable types
            landLockedCountryStyle.StaticTypes.Add(typeof(System.Convert));
            // The math class might be handy to include but in this sample we do not use it
            //landLockedCountryStyle.StaticTypes.Add(typeof(System.Math));

            landLockedCountryStyle.ColumnVariables.Add("POP_CNTRY");
            landLockedCountryStyle.ColumnVariables.Add("LANDLOCKED");            

            landLockedCountryStyle.CustomTrueStyles.Add(new AreaStyle(AreaStyles.Country1.OutlinePen, new GeoSolidBrush(GeoColor.SimpleColors.Yellow)));
            landLockedCountryStyle.CustomFalseStyles.Add(AreaStyles.Country1);            
            
            ShapeFileFeatureLayer worldLayer = new ShapeFileFeatureLayer(@"..\..\..\Dependencies\Countries02.shp", ShapeFileReadWriteMode.ReadOnly);
            worldLayer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(landLockedCountryStyle);
            worldLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;

 



using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Ciloci.Flee;

namespace ThinkGeo.MapSuite.Core
{
    public class FleeBooleanStyle : Style
    {
        private string fleeExpression;
        private Collection<Style> customTrueStyles;
        private Collection<Style> customFalseStyles;
        private Dictionary<string, object> userVariables;
        private Collection<string> columnVariables;
        private Collection<Type> staticTypes;

        public FleeBooleanStyle()
            : this(string.Empty, new Collection<Style>(), new Collection<Style>())
        {
        }

        public FleeBooleanStyle(string fleeExpression)
            : this(fleeExpression, new Collection<Style>(), new Collection<Style>())
        {
        }

        private FleeBooleanStyle(string fleeExpression, Collection<Style> trueCustomStyles, Collection<Style> falseCustomStyles)
        {
            this.fleeExpression = fleeExpression;
            this.customTrueStyles = trueCustomStyles;
            this.customFalseStyles = falseCustomStyles;
            userVariables = new Dictionary<string, object>();
            columnVariables = new Collection<string>();
            staticTypes = new Collection<Type>();
        }

        public string FleeExpression
        {
            get { return fleeExpression; }
            set { fleeExpression = value; }
        }

        public Collection<Type> StaticTypes
        {
            get { return staticTypes; }            
        }

        public Collection<string> ColumnVariables
        {
            get { return columnVariables; }            
        }

        public Dictionary<string, object> UserVariables
        {
            get { return userVariables; }            
        }

        public Collection<Style> CustomTrueStyles
        {
            get { return customTrueStyles; }
        }

        public Collection<Style> CustomFalseStyles
        {
            get { return customFalseStyles; }
        }

        protected override void DrawCore(IEnumerable<Feature> features, GeoCanvas canvas, Collection<SimpleCandidate> labelsInThisLayer, Collection<SimpleCandidate> labelsInAllLayers)
        {
            ExpressionContext context = new ExpressionContext();

            // Add all of the user variables
            foreach (string customVariableKey in userVariables.Keys)
            {
                context.Variables.Add(customVariableKey, userVariables[customVariableKey]);
            }

            // Add all of the column variables
            foreach (string columnVariable in columnVariables)
            {
                context.Variables.Add(columnVariable, string.Empty);
            }

            // Add all of our static types we need
            foreach (Type staticType in staticTypes)
            {
                context.Imports.AddType(staticType);
            }

            IGenericExpression<bool> e = context.CompileGeneric<bool>(fleeExpression);

            foreach (Feature feature in features)
            {
                // update the variables we get from the feature
                foreach (string columnName in columnVariables)
                {
                    context.Variables[columnName] = feature.ColumnValues[columnName];
                }

                bool evaluatedTrue = e.Evaluate();

                if (evaluatedTrue)
                {
                    foreach (Style style in customTrueStyles)
                    {
                        style.Draw(new Collection<Feature>() { feature }, canvas, labelsInThisLayer, labelsInAllLayers);
                    }
                }
                else
                {
                    foreach (Style style in customFalseStyles)
                    {
                        style.Draw(new Collection<Feature>() { feature }, canvas, labelsInThisLayer, labelsInAllLayers);
                    }
                }
            }
        }

        protected override Collection<string> GetRequiredColumnNamesCore()
        {
            Collection<string> requiredFieldNames = new Collection<string>();

            // Column Variables
            foreach (string columnName in columnVariables)
            {
                if (!requiredFieldNames.Contains(columnName))
                {
                    requiredFieldNames.Add(columnName);
                }
            }

            // Custom True Styles
            foreach (Style style in customTrueStyles)
            {
                Collection<string> tmpCollection = style.GetRequiredColumnNames();

                foreach (string name in tmpCollection)
                {
                    if (!requiredFieldNames.Contains(name))
                    {
                        requiredFieldNames.Add(name);
                    }
                }
            }

            // Custom False Styles
            foreach (Style style in customFalseStyles)
            {
                Collection<string> tmpCollection = style.GetRequiredColumnNames();

                foreach (string name in tmpCollection)
                {
                    if (!requiredFieldNames.Contains(name))
                    {
                        requiredFieldNames.Add(name);
                    }
                }
            }

            return requiredFieldNames;
        }
    }
}