Skip to content

Extensibility Guide

Integrating Custom Data Formats

using ThinkGeo.Core;

namespace OledbPointFeatureSource
{
    class MinimumFeatureLayer : FeatureLayer
    {
        public MinimumFeatureLayer()
        {
            this.FeatureSource = new MinimumFeatureSource();
        }
    }
}
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ThinkGeo.Core;

namespace OledbPointFeatureSource
{
    class MinimumFeatureSource : FeatureSource
    {
        protected override Collection<Feature> GetAllFeaturesCore(IEnumerable<string> columnNames)
        {
            return new Collection<Feature>(new Feature[] { new Feature(-95.2806, 38.9554, "1") });
        }
    }
}
using ThinkGeo.MapSuite.Core;

namespace OledbPointFeatureSource
{
    // The FeatureLayer is a wrapper to the FeatureSource and provides all of the drawing specific
    // methods such as the ZoomLevels and Styles.  As you can see it is just a simple wrapper and the
    // real work is already done for you in the FeatureLayer base class.
    class OledbPointFeatureLayer: FeatureLayer
    {
        OledbPointFeatureSource oledbPointFeatureSource;

        // It is important for compatability that you always have a default constructor that
        // takes no parameters.  This constructor will just chain to the more complex one.
        public OledbPointFeatureLayer()
            : this(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty)
        { }

        // We have provided another constructor that has all of the parameters we need for the FeatureLayer
        // to work.
        public OledbPointFeatureLayer(string tableName, string idColumnName, string xColumnName, string yColumnName, string connectionString)
            : base()
        {
            // Here is where we create our FeatureSource and set the internal property on the FeatureLayer
            oledbPointFeatureSource = new OledbPointFeatureSource(tableName, idColumnName, xColumnName, yColumnName, connectionString);
            this.FeatureSource = oledbPointFeatureSource;           
        }

        // The next dozen lines are all of the properties we need.  It is good form to always match
        // the parameters in your constructors with properties of the exact same name.
        public string TableName
        {
            get { return oledbPointFeatureSource.TableName; }
            set { oledbPointFeatureSource.TableName = value; }
        }

        public string ConnectionString
        {
            get { return oledbPointFeatureSource.ConnectionString; }
            set { oledbPointFeatureSource.ConnectionString = value; }
        }

        public string XColumnName
        {
            get { return oledbPointFeatureSource.XColumnName; }
            set { oledbPointFeatureSource.XColumnName = value; }
        }

        public string YColumnName
        {
            get { return oledbPointFeatureSource.YColumnName; }
            set { oledbPointFeatureSource.YColumnName = value; }
        }

        public string IdColumnName
        {
            get { return oledbPointFeatureSource.IdColumnName; }
            set { oledbPointFeatureSource.IdColumnName = value; }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.OleDb;
using ThinkGeo.Core;

namespace OledbPointFeatureSource
{
    class OledbPointFeatureSource : FeatureSource
    {
        private OleDbConnection connection;
        private string connectionString;
        private string tableName;
        private string idColumnName;
        private string xColumnName;
        private string yColumnName;

        // It is important for compatability that you always have a default constructor that
        // takes no parameters.  This constructor will just chain to the more complex one.
        public OledbPointFeatureSource()
            : this(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty)
        { }

        // We have provided another constructor that has all of the parameters we need for the FeatureSource
        // to work.
        public OledbPointFeatureSource(string tableName, string idColumnName, string xColumnName, string yColumnName, string connectionString)
            : base()
        {
            TableName = tableName;
            IdColumnName = idColumnName;
            XColumnName = xColumnName;
            YColumnName = yColumnName;
            ConnectionString = connectionString;
        }

        // The next dozen lines are all of the properties we need.  It is good form to always match
        // the parameters in your constructors with properties of the exact same name.
        public string TableName
        {
            get { return tableName; }
            set { tableName = value; }
        }

        public string ConnectionString
        {
            get { return connectionString; }
            set { connectionString = value; }
        }

        public string XColumnName
        {
            get { return xColumnName; }
            set { xColumnName = value; }
        }

        public string YColumnName
        {
            get { return yColumnName; }
            set { yColumnName = value; }
        }

        public string IdColumnName
        {
            get { return idColumnName; }
            set { idColumnName = value; }
        }

        // Use the OpenCore to initialize your underlying data source.  The concreat Open method will ensure
        // that if the user calls the Open method multiple times in a row that you only get one call to 
        // OpenCore.
        protected override void OpenCore()
        {
            connection = new OleDbConnection(connectionString);
            connection.Open();
        }

        // Use the CloseCore to close your underlying data source.  It is important to note that the
        // CloseCore is not like the Dispose on other objects.  The FeatureSource is meant to be opened
        // and closed many times durring its lifetime.  Make sure that you clean up any objects that have 
        // unmanaged resources but do not put the object in a state that when Open is called it will fail.
        // The concreat Close method will ensure that if the user calle the Close multiple times in 
        // a row that you only get one call to CloseCore.
        protected override void CloseCore()
        {
            connection.Close();
        }

        // Here you need to query all of the features in your data source.  We use this method
        // as the basis of nearly all other virtual methods.  For example if you choose not to
        // override the GetCountCore then we will get all the features and count them as the default.
        // This is ineffecient however it produces the correct results.
        protected override Collection<Feature> GetAllFeaturesCore(IEnumerable<string> columnNames)
        {
            OleDbCommand command = null;
            OleDbDataReader dataReader = null;
            Collection<Feature> returnFeatures = new Collection<Feature>();

            // Alays be sure to wrap imporant code that accesses resources that need
            // to be closed.  In the Finally we will ensure they always get cleaned up.
            try
            {
                // We need to construct the part of the SQL statement for retuning the
                // column data.  We only return the columns asked for byt the columnNames
                // parameter of the function.  This ensures we do not get more than we need.
                string columnsSQL = string.Empty;
                foreach (string columnName in columnNames)
                {
                    columnsSQL += "," + columnName;
                }

                // Here we build up and execute the query string using the columns the users defined in the properties
                // such as the XColumnName and IdColumnName.
                command = new OleDbCommand("SELECT " + idColumnName + " as TG_ID, " + xColumnName + " as TG_X, " + yColumnName + " as TG_Y " + columnsSQL + " FROM " + tableName, connection);
                dataReader = command.ExecuteReader();

                // We now loop though all of the results and build up our features that we need to return.
                while (dataReader.Read())
                {
                    Feature feature = new Feature(new PointShape(Convert.ToDouble(dataReader["TG_X"]), Convert.ToDouble(dataReader["TG_Y"])), dataReader["TG_ID"].ToString());

                    // This small part populates the column values that were requested.  They are data
                    // such as columns used for ClassBreakStyles or TextStyles for labeling.
                    foreach (string columnName in columnNames)
                    {
                        feature.ColumnValues.Add(columnName, dataReader[columnName].ToString());
                    }
                    returnFeatures.Add(feature);
                }
            }
            finally
            {
                // Cleanup any of the objects that need to be closed or disposed.
                if (command != null) { command.Dispose(); }
                if (dataReader != null) { dataReader.Dispose(); }
            }

            return returnFeatures;
        }

        // Though this method is not required for creating our new class it is an important one.
        // This method is used to get only the features inside of the bounding box passed in.  The reason
        // this is critical is that many other methods on the QueryTools such as Touches, Overlaps, Intersects,
        // and many others use this method as a first pass filter.  If you do not override this method then
        // the default code calls the GetAllFeatures and look at each to see if it is in the bounding box.
        // While this method will produce the correct result it will not perform as well as your custom code.
        protected override Collection<Feature> GetFeaturesInsideBoundingBoxCore(RectangleShape boundingBox, IEnumerable<string> returningColumnNames)
        {
            OleDbCommand command = null;
            OleDbDataReader dataReader = null;
            Collection<Feature> returnFeatures = new Collection<Feature>();

            // Alays be sure to wrap imporant code that accesses resources that need
            // to be closed.  In the Finally we will ensure they always get cleaned up.
            try
            {
                // We need to construct the part of the SQL statement for retuning the
                // column data.  We only return the columns asked for byt the columnNames
                // parameter of the function.  This ensures we do not get more than we need.
                string columnsSQL = string.Empty;
                foreach (string columnName in returningColumnNames)
                {
                    columnsSQL += "," + columnName;
                }

                // This whereSql is important becasue it is what is used to determine if the point is in
                // the bounding box passed in.  While it is a bit complex it get your results quickly in
                // large datasets so long as the X & Y columns are indexed.
                string whereSql = " " + xColumnName + " <= " + boundingBox.LowerRightPoint.X + " AND " + xColumnName + " >= " + boundingBox.UpperLeftPoint.X + " AND " + yColumnName + " <= " + boundingBox.UpperLeftPoint.Y + " AND " + yColumnName + " >= " + boundingBox.LowerRightPoint.Y; 

                command = new OleDbCommand("SELECT " + idColumnName + " as TG_ID, " + xColumnName + " as TG_X, " + yColumnName + " as TG_Y " + columnsSQL + " FROM " + tableName + " WHERE " + whereSql, connection);
                dataReader = command.ExecuteReader();

                // We now loop though all of the results and build up our features that we need to return.
                while (dataReader.Read())
                {
                    Feature feature = new Feature(Convert.ToDouble(dataReader["TG_X"]), Convert.ToDouble(dataReader["TG_Y"]), dataReader["TG_ID"].ToString());

                    // This small part populates the column values that were requested.  They are data
                    // such as columns used for ClassBreakStyles or TextStyles for labeling.
                    foreach (string columnName in returningColumnNames)
                    {
                        feature.ColumnValues.Add(columnName, dataReader[columnName].ToString());
                    }
                    returnFeatures.Add(feature);
                }
            }
            finally
            {
                // Cleanup any of the objects that need to be closed or disposed.
                if (command != null) { command.Dispose(); }
                if (dataReader != null) { dataReader.Dispose(); }
            }

            return returnFeatures;            
        }

        // This method returns all of the columns in the data source.  This method is not required however
        // if it is not overridden then the FeatureSource will not have any columns available to it.
        // Since having access to the column data is usefull for labeling and such we suggest you override it.
        protected override Collection<FeatureSourceColumn> GetColumnsCore()
        {
            OleDbCommand command = null;
            OleDbDataReader dataReader = null;
            Collection<FeatureSourceColumn> returnColumns = new Collection<FeatureSourceColumn>();

            // Alays be sure to wrap imporant code that accesses resources that need
            // to be closed.  In the Finally we will ensure they always get cleaned up.
            try
            {
                // Here we have a query that will quickly return nothing.  As strange as it sounds it is a
                // good way to get the table structure back without having to return any column data.
                command = new OleDbCommand("SELECT * FROM " + tableName + " WHERE 1=2", connection);
                dataReader = command.ExecuteReader();

                // We now loop through and create our column list.  In the FeatureSourceColumn we can
                // optionally provide the column type but it is just informational so we didn't code it.
                for (int i = 0; i < dataReader.FieldCount; i++)
                {
                    FeatureSourceColumn featureSourceColumn = new FeatureSourceColumn(dataReader.GetName(i));
                    returnColumns.Add(featureSourceColumn);
                }
            }
            finally
            {
                // Cleanup any of the objects that need to be closed or disposed.
                if (command != null) { command.Dispose(); }
                if (dataReader != null) { dataReader.Dispose(); }
            }

            return returnColumns;
        }        

        // This is another method that does not need to be overridden but we suggest that you do.
        // This method gets the cound of all the features in the data source.  If you choose not to 
        // override it then the default will call the GetAllFeatures and count them.  This is not very
        // effecient so we suggest you override it.
        protected override int GetCountCore()
        {
            OleDbCommand command = null;            
            int count = 0;

            // Alays be sure to wrap imporant code that accesses resources that need
            // to be closed.  In the Finally we will ensure they always get cleaned up.
            try
            {
                // Here we do a standard SQL count statement and return the results.
                command = new OleDbCommand("SELECT COUNT(*) FROM " + tableName, connection);
                count = Convert.ToInt32(command.ExecuteScalar());                
            }
            finally
            {
                // Cleanup any of the objects that need to be closed or disposed.
                if (command != null) { command.Dispose(); }                
            }
            return count;
        }
    }
}

Creating Custom Styles

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

namespace ExtendingMapSuiteStyle
{
    // Mark the class Serializable so that it workes in SQL Server state
    // and in every serialization context
    [Serializable]
    class SizedPointStyle : Style
    {
        private PointStyle pointStyle;
        private float ratio;
        private string sizeColumnName;

        public SizedPointStyle()
            : this(new PointStyle(), string.Empty, 1)
        { }

        public SizedPointStyle(PointStyle pointStyle, string sizeColumnName, float ratio)
        {
            this.pointStyle = pointStyle;
            this.sizeColumnName = sizeColumnName;
            this.ratio = ratio;
        }

        public PointStyle PointStyle
        {
            get { return pointStyle; }
            set { pointStyle = value; }
        }

        public float Ratio
        {
            get { return ratio; }
            set { ratio = value; }
        }

        public string SizeColumnName
        {
            get { return sizeColumnName; }
            set { sizeColumnName = value; }
        }

        protected override void DrawCore(IEnumerable<Feature> features, GeoCanvas canvas, Collection<SimpleCandidate> labelsInThisLayer, Collection<SimpleCandidate> labelsInAllLayers)
        {
            // Loop through each feaure and determine how large the point should 
            // be then adjust it's size.
            foreach (Feature feature in features)
            {
                float sizeData = Convert.ToSingle(feature.ColumnValues[sizeColumnName]);
                float symbolSize = sizeData / ratio;
                pointStyle.SymbolSize = symbolSize;
                pointStyle.Draw(new Collection<Feature>() { feature }, canvas, labelsInThisLayer, labelsInAllLayers);
            }
        }

        protected override Collection<string> GetRequiredColumnNamesCore()
        {
            // Here we grab the columns from the pointStyle and then add
            // the sizeColumn name to make sure we pull back the column
            //  that we need to calculate the size
            Collection<string> columns = new Collection<string>();
            columns = pointStyle.GetRequiredColumnNames();
            if (!columns.Contains(sizeColumnName))
            {
                columns.Add(sizeColumnName);
            }

            return columns;
        }

        protected override void DrawSampleCore(GeoCanvas canvas)
        {
            // Here we simply pass the call to the pointStyle's DrawSampleCore
            pointStyle.DrawSample(canvas);
        }
    }
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ThinkGeo.Core;

namespace ExtendingMapSuiteStyle
{
    // Mark the class Serializable so that it workes in SQL Server state
    // and in every serialization context
    [Serializable]
    class TimeBasedPointStyle : Style
    {
        private PointStyle daytimePointStyle;
        private PointStyle nighttimePointStyle;
        private string timeZoneColumnName;

        public TimeBasedPointStyle()
            : this(string.Empty, new PointStyle(), new PointStyle())
        { }

        public TimeBasedPointStyle(string timeZoneColumnName, PointStyle daytimePointStyle, PointStyle nighttimePointStyle)
        {
            this.timeZoneColumnName = timeZoneColumnName;
            this.daytimePointStyle = daytimePointStyle;
            this.nighttimePointStyle = nighttimePointStyle;
        }

        public PointStyle DaytimePointStyle
        {
            get { return daytimePointStyle; }
            set { daytimePointStyle = value; }
        }

        public PointStyle NighttimePointStyle
        {
            get { return nighttimePointStyle; }
            set { nighttimePointStyle = value; }
        }

        public string TimeZoneColumnName
        {
            get { return timeZoneColumnName; }
            set { timeZoneColumnName = value; }
        }


        protected override void DrawCore(IEnumerable<Feature> features, GeoCanvas canvas, Collection<SimpleCandidate> labelsInThisLayer, Collection<SimpleCandidate> labelsInAllLayers)
        {
            foreach (Feature feature in features)
            {
                // Here we are going to do the calculation to see what
                // time it is for each feature and draw the approperate style
                float offsetToGmt = Convert.ToSingle(feature.ColumnValues[timeZoneColumnName]);
                DateTime localTime = DateTime.UtcNow.AddHours(offsetToGmt);
                if (localTime.Hour >= 7 && localTime.Hour <= 19)
                {
                    // Daytime
                    daytimePointStyle.Draw(new Collection<Feature>() { feature }, canvas, labelsInThisLayer, labelsInAllLayers);
                }
                else
                {
                    //Nighttime

                    nighttimePointStyle.Draw(new Collection<Feature>() { feature }, canvas, labelsInThisLayer, labelsInAllLayers);
                }
            }
        }

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

            // Grab any columns that the daytime style may need.
            Collection<string> daytimeColumns = daytimePointStyle.GetRequiredColumnNames();
            foreach (string column in daytimeColumns)
            {
                if (!columns.Contains(column))
                {
                    columns.Add(column);
                }
            }

            // Grab any columns that the nighttime style may need.
            Collection<string> nighttimeColumns = nighttimePointStyle.GetRequiredColumnNames();
            foreach (string column in nighttimeColumns)
            {
                if (!columns.Contains(column))
                {
                    columns.Add(column);
                }
            }

            // Make sure we add the timezone column
            if (!columns.Contains(timeZoneColumnName))
            {
                columns.Add(timeZoneColumnName);
            }

            return columns;
        }

        protected override void DrawSampleCore(GeoCanvas canvas)
        {
            // Here is an example of why it is hard
            // to always draw a sample.  In this case
            // we just draw the daytime stlye.
            daytimePointStyle.DrawSample(canvas);
        }
    }
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ThinkGeo.Core;

namespace ExtendingMapSuiteStyle
{
    // Mark the class Serializable so that it workes in SQL Server state
    // and in every serialization context
    [Serializable]
    public class CachedValueStyle : Style
    {
        private string columnName;
        private Dictionary<string, ValueItem> valueItems = new Dictionary<string, ValueItem>();
        private Dictionary<string, string> valuesCache = new Dictionary<string, string>();

        public CachedValueStyle()
            : this(string.Empty, new Dictionary<string, ValueItem>())
        { }

        public CachedValueStyle(string columnName, Dictionary<string, ValueItem> valueItems)
        {
            this.columnName = columnName;
            this.valueItems = valueItems;
        }

        public string ColumnName
        {
            get { return columnName; }
            set { columnName = value; }
        }

        // We use a dictionary now instead of a collection to make lookups faster.
        public Dictionary<string, ValueItem> ValueItems
        {
            get { return valueItems; }
        }

        // Here you can pre-load all of the values by feature Id.
        // The key is the Feature.Id and the value is the value to match the
        // value on.
        public Dictionary<string, string> ValuesCache
        {
            get { return valuesCache; }
        }

        protected override void DrawCore(IEnumerable<Feature> features, GeoCanvas canvas, Collection<SimpleCandidate> labelsInThisLayer, Collection<SimpleCandidate> labelsInAllLayers)
        {
            foreach (Feature feature in features)
            {
                string fieldValue = string.Empty;

                // If we have items field value in the cache then let's use them!
                // If not then get the value from the feature's column values
                if (valuesCache.Count == 0)
                {
                    fieldValue = feature.ColumnValues[columnName].Trim();
                }
                else
                {
                    if (!ValuesCache.TryGetValue(feature.Id, out fieldValue))
                    {
                        fieldValue = string.Empty;
                    }
                }

                ValueItem valueItem = null;
                // Check if the there is a value item in the dictionary that matches 
                // our field value.
                if (valueItems.ContainsKey(fieldValue))
                {
                    valueItem = valueItems[fieldValue];
                }

                // If we cannot find a matcing value item in the collection do not draw anything.
                if (valueItem != null)
                {
                    // Call the draw on all of the default styles of the value item and also
                    // check if there are custom styles
                    Feature[] tmpFeatures = new Feature[1] { feature };
                    if (valueItem.CustomStyles.Count == 0)
                    {
                        valueItem.DefaultAreaStyle.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                        valueItem.DefaultLineStyle.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                        valueItem.DefaultPointStyle.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                        valueItem.DefaultTextStyle.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                    }
                    else
                    {
                        foreach (Style style in valueItem.CustomStyles)
                        {
                            style.Draw(tmpFeatures, canvas, labelsInThisLayer, labelsInAllLayers);
                        }
                    }
                }
            }
        }

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

            // If we have provided cached values then there is no need to fetch the
            // column from our feature source.
            if (valuesCache.Count == 0)
            {
                requiredFieldNames.Add(columnName);
            }

            foreach (ValueItem valueItem in valueItems.Values)
            {
                // Check if we need any columns from custom styles
                foreach (Style style in valueItem.CustomStyles)
                {
                    Collection<string> tmpCollection = style.GetRequiredColumnNames();

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

                // Check if we need any columns from the DefaultTextStyle
                Collection<string> fieldsInTextStyle = valueItem.DefaultTextStyle.GetRequiredColumnNames();
                foreach (string fieldName in fieldsInTextStyle)
                {
                    if (!requiredFieldNames.Contains(fieldName))
                    {
                        requiredFieldNames.Add(fieldName);
                    }
                }

                // Check if we need any columns from the DefaultPointStyle
                Collection<string> fieldsInPointStyle = valueItem.DefaultPointStyle.GetRequiredColumnNames();
                foreach (string fieldName in fieldsInPointStyle)
                {
                    if (!requiredFieldNames.Contains(fieldName))
                    {
                        requiredFieldNames.Add(fieldName);
                    }
                }

                // Check if we need any columns from the DefaultLineStyle
                Collection<string> fieldsInLineStyle = valueItem.DefaultLineStyle.GetRequiredColumnNames();
                foreach (string fieldName in fieldsInLineStyle)
                {
                    if (!requiredFieldNames.Contains(fieldName))
                    {
                        requiredFieldNames.Add(fieldName);
                    }
                }

                // Check if we need any columns from the DefaultAreaStyle
                Collection<string> fieldsInAreaStyle = valueItem.DefaultAreaStyle.GetRequiredColumnNames();
                foreach (string fieldName in fieldsInAreaStyle)
                {
                    if (!requiredFieldNames.Contains(fieldName))
                    {
                        requiredFieldNames.Add(fieldName);
                    }
                }
            }

            return requiredFieldNames;
        }
    }
}

Exploring Layers

using ThinkGeo.Core;

namespace Samples
{
    // This is our MapShape and it will be the single unit in our layer.
    public class MapShape
    {
        private Feature feature;
        private ZoomLevelSet zoomLevelSet;

        public MapShape()
            : this(new Feature())
        {
        }

        // Let's use this as a handy constructor if you already have
        // a feature or want to create one inline.
        public MapShape(Feature feature)
        {
            this.feature = feature;
            zoomLevelSet = new ZoomLevelSet();
        }

        //  This is the feature property, pretty simple.
        public Feature Feature
        {
            get { return feature; }
            set { feature = value; }
        }

        // This is the Zoom Level Set.  This high level object has all of
        // the logic in it for zoom levels, drawing and everything.
        public ZoomLevelSet ZoomLevels
        {
            get { return zoomLevelSet; }
            set { zoomLevelSet = value; }
        }
    }
}
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ThinkGeo.Core;

namespace Samples
{
    public class MapShapeLayer : Layer
    {
        private Dictionary<string, MapShape> mapShapes;

        public MapShapeLayer()
        {
            this.mapShapes = new Dictionary<string, MapShape>();
        }

        // Here is where you place all of your map shapes.
        public Dictionary<string, MapShape> MapShapes
        {
            get { return mapShapes; }
        }

        // This is a required overload of the Layer.  As you can see we simply
        // loop through all of our map shapes and then choose the correct zoom level.
        // After that, the zoom level class takes care of the heavy lifiting.  You
        // have to love how easy this framework is to re-use.
        protected override void DrawCore(GeoCanvas canvas, Collection<SimpleCandidate> labelsInAllLayers)
        {
            foreach (string mapShapeKey in mapShapes.Keys)
            {
                MapShape mapShape = mapShapes[mapShapeKey];
                ZoomLevel currentZoomLevel = mapShape.ZoomLevels.GetZoomLevelForDrawing(canvas.CurrentWorldExtent, canvas.Width, canvas.MapUnit);
                if (currentZoomLevel != null)
                {
                    if (canvas.CurrentWorldExtent.Intersects(mapShape.Feature.GetBoundingBox()))
                    {
                        currentZoomLevel.Draw(canvas, new Feature[] { mapShape.Feature }, new Collection<SimpleCandidate>(), labelsInAllLayers);
                    }
                }
            }
        }
    }
}
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ThinkGeo.Core;

namespace Samples
{
    public class MiniMapAdornmentLayer : AdornmentLayer
    {
        private Collection<Layer> layers;
        private int width;
        private int height;

        public MiniMapAdornmentLayer()
            : this(new Layer[] { }, 100, 100)
        { } 

        public MiniMapAdornmentLayer(int width, int height)
            : this(new Layer[] { }, width, height)
        { }

        public MiniMapAdornmentLayer(IEnumerable<Layer> layers, int width, int height)
        {
            this.layers = new Collection<Layer>();
            foreach (Layer layer in layers)
            {
                this.layers.Add(layer);
            }

            this.width = width;
            this.height = height;
        }

        public Collection<Layer> Layers
        {
            get { return layers; }
        }

        public int Width
        {
            get { return width; }
            set { width = value; }
        }

        public int Height
        {
            get { return height; }
            set { height = value; }
        }

        protected override void DrawCore(GeoCanvas canvas, Collection<SimpleCandidate> labelsInAllLayers)
        {
            GeoImage miniImage = new GeoImage(width, height);
            RectangleShape scaledWorldExtent = MapEngine.GetDrawingExtent(canvas.CurrentWorldExtent, width, height);
            scaledWorldExtent.ScaleUp(300);
            GdiPlusGeoCanvas minCanvas = new GdiPlusGeoCanvas();

            minCanvas.BeginDrawing(miniImage, scaledWorldExtent, canvas.MapUnit);
            foreach (Layer layer in layers)
            {
                layer.Draw(minCanvas, labelsInAllLayers);
            }

            minCanvas.DrawArea(RectangleShape.ScaleDown(minCanvas.CurrentWorldExtent, 1), new GeoPen(GeoColor.StandardColors.Gray, 2), DrawingLevel.LevelOne);
            minCanvas.DrawArea(canvas.CurrentWorldExtent, new GeoPen(GeoColor.StandardColors.Black, 2), DrawingLevel.LevelOne);

            minCanvas.EndDrawing();

            ScreenPointF drawingLocation = GetDrawingLocation(canvas, width, height);

            canvas.DrawScreenImageWithoutScaling(miniImage, (drawingLocation.X + width / 2) + 10, (drawingLocation.Y + height / 2) - 10, DrawingLevel.LevelOne, 0, 0, 0);
        }
    }
}
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.IO;
using GisSharpBlog.NetTopologySuite.Geometries;
using GisSharpBlog.NetTopologySuite.Index.Strtree;
using ThinkGeo.Core;

namespace Samples
{
    // IMPORTANT: You have to reference NetTopologySuite.dll & GeoApi.dll in your project.  They come with Map Suite.

    // This class speeds up the loading of a large number of Raster layers by loading and drawing on demand only the files
    // in the current extent.  Normally if you had 100 Raster files you would need to load 100 layers; however, this has performance
    // issues--so we created this high level layer.  It loads a reference file that contains the bounding box, path and file information for 
    // all of the Raster files.  We load this information into an in-memory spatial index. When the map requests to draw the layer, we find the
    // Rasters that are in the current extent, create a layer on-the-fly, call their Draw method and then close them.  In this way, we load
    // on demand only the files that are in the current extent.

    // I have also included a small routine to build a reference file from a directory of Raster files.

    // Reference File Format: [UpperLeftPointX],[LowerRightPoint.X],[UpperLeftPoint.Y],[LowerRightPoint.Y],[Path & File Name to Raster]    

    // Sample:
    // -180.02197265625,-157.52197265625,-67.47802734375,-89.97802734375,..\..\App_Data\RasterImage\1.jpg
    // -112.52197265625,-90.02197265625,-67.47802734375,-89.97802734375,..\..\App_Data\RasterImage\10.jpg
    // 67.47802734375,89.97802734375,67.52197265625,45.02197265625,..\..\App_Data\RasterImage\100.jpg
    // 89.97802734375,112.47802734375,67.52197265625,45.02197265625,..\..\App_Data\RasterImage\101.jpg

    public class MultiGeoRasterLayer : Layer
    {
        private string rasterRefrencePathFileName;
        private STRtree spatialIndex;
        private double upperScale;
        private double lowerScale;
        private RectangleShape boundingBox;

        private const int upperLeftXPosition = 0;
        private const int upperLeftYPosition = 2;
        private const int lowerRightXPosition = 1;
        private const int lowerRightYPosition = 3;
        private const int pathFileNamePosition = 4;

        public MultiGeoRasterLayer()
            : this(string.Empty, double.MaxValue, double.MinValue)
        { }

        public MultiGeoRasterLayer(string rasterRefrencePathFileName)
            : this(rasterRefrencePathFileName, double.MaxValue, double.MinValue)
        { }

        public MultiGeoRasterLayer(string rasterRefrencePathFileName, double upperScale, double lowerScale)
        {
            this.rasterRefrencePathFileName = rasterRefrencePathFileName;
            this.upperScale = upperScale;
            this.lowerScale = lowerScale;
            boundingBox = new RectangleShape();
        }

        public string RasterRefrencePathFileName
        {
            get { return rasterRefrencePathFileName; }
            set { rasterRefrencePathFileName = value; }
        }

        public double UpperScale
        {
            get { return upperScale; }
            set { upperScale = value; }
        }

        public double LowerScale
        {
            get { return lowerScale; }
            set { lowerScale = value; }
        }

        // Here on the OpenCore we load our reference file and build the spatial index, which will be used in the DrawCore later.
        // You need to make sure the reference file is in the right format as described in the comments above.
        protected override void OpenCore()
        {
            if (File.Exists(rasterRefrencePathFileName))
            {
                string[] rasterFiles = File.ReadAllLines(rasterRefrencePathFileName);
                spatialIndex = new STRtree(rasterFiles.Length);

                Collection<BaseShape> boundingBoxes = new Collection<BaseShape>();

                foreach (string rasterLine in rasterFiles)
                {
                    string[] parts = rasterLine.Split(new string[] { "," }, StringSplitOptions.None);
                    RectangleShape rasterBoundingBox = new RectangleShape(new PointShape(double.Parse(parts[upperLeftXPosition]), double.Parse(parts[upperLeftYPosition])), new PointShape(double.Parse(parts[lowerRightXPosition]), double.Parse(parts[lowerRightYPosition])));
                    Envelope envelope = new Envelope(double.Parse(parts[upperLeftXPosition]), double.Parse(parts[lowerRightXPosition]), double.Parse(parts[upperLeftYPosition]), double.Parse(parts[lowerRightYPosition]));
                    spatialIndex.Insert(envelope, parts[pathFileNamePosition]);
                }
                spatialIndex.Build();
                boundingBox = ExtentHelper.GetBoundingBoxOfItems(boundingBoxes);
            }
            else
            {
                throw new FileNotFoundException("The Raster reference file could not be found.", rasterRefrencePathFileName);
            }
        }

        // Here we set the spatial index to null to clean up the memory and get ready for serialization
        protected override void CloseCore()
        {
            spatialIndex = null;
        }

        // When we get to the Draw, things are easy.  First we check to make sure we are within our scales.
        // Next we look up the Raster files in the spatial index,
        // then open their layer, call their Draw and close them.
        protected override void DrawCore(GeoCanvas canvas, Collection<SimpleCandidate> labelsInAllLayers)
        {
            double currentScale = ExtentHelper.GetScale(canvas.CurrentWorldExtent, canvas.Width, canvas.MapUnit);

            if (currentScale >= lowerScale && currentScale <= upperScale)
            {
                RectangleShape currentExtent = canvas.CurrentWorldExtent;
                Envelope currentExtentEnvelope = new Envelope(currentExtent.UpperLeftPoint.X, currentExtent.LowerRightPoint.X, currentExtent.UpperLeftPoint.Y, currentExtent.LowerRightPoint.Y);
                ArrayList rasters = (ArrayList)spatialIndex.Query(currentExtentEnvelope);

                foreach (string file in rasters)
                {
                    GdiPlusRasterLayer rasterRasterLayer = new GdiPlusRasterLayer(file);
                    rasterRasterLayer.Open();
                    rasterRasterLayer.Draw(canvas, labelsInAllLayers);
                    rasterRasterLayer.Close();
                }
            }
        }

        // Here we let everyone know we support having a bounding box
        public override bool HasBoundingBox
        {
            get
            {
                return true;
            }
        }

        //  We use the cached bounding box we set in the OpenCore
        protected override RectangleShape GetBoundingBoxCore()
        {
            return boundingBox;
        }

        // This is just a handy function to build a reference file from a directory.
        // You can tailor this code to fit your needs.
        public static void BuildReferenceFile(string newReferencepathFileName, string pathOfRasterFiles)
        {
            if (Directory.Exists(pathOfRasterFiles))
            {
                string[] files = Directory.GetFiles(pathOfRasterFiles, "*.jpg");
                StreamWriter streamWriter = null;

                try
                {
                    streamWriter = File.CreateText(newReferencepathFileName);

                    foreach (string file in files)
                    {
                        GdiPlusRasterLayer rasterRasterLayer = new GdiPlusRasterLayer(file);
                        rasterRasterLayer.Open();
                        RectangleShape boundingBox = rasterRasterLayer.GetBoundingBox();
                        rasterRasterLayer.Close();
                        streamWriter.WriteLine(string.Format("{0},{1},{2},{3},{4}", boundingBox.UpperLeftPoint.X, boundingBox.LowerRightPoint.X, boundingBox.UpperLeftPoint.Y, boundingBox.LowerRightPoint.Y, file));
                    }
                    streamWriter.Close();
                }
                finally
                {
                    if (streamWriter != null) { streamWriter.Dispose(); }
                }
            }
            else
            {
                throw new DirectoryNotFoundException("The path containing the Raster files could not be found.");
            }
        }
    }
}
using System.Collections.ObjectModel;
using ThinkGeo.Core;

namespace Samples
{
    public class WatermarkAdornmentLayer : AdornmentLayer
    {
        private string watermarkText = string.Empty;

        public WatermarkAdornmentLayer()
            : this("Watermark")
        { }

        public WatermarkAdornmentLayer(string watermarkText)
            : base()
        {
            this.watermarkText = watermarkText;
        }

        public string WatermarkText
        {
            get { return watermarkText; }
            set { watermarkText = value; }
        }

        protected override void DrawCore(GeoCanvas canvas, Collection<SimpleCandidate> labelsInAllLayers)
        {
            GeoFont font = new GeoFont("Arial", 12);
            GeoBrush brush = new GeoSolidBrush(GeoColor.StandardColors.Pink);
            Collection<ScreenPointF> textPath = new Collection<ScreenPointF>();

            int interval = 100;
            if (canvas.Width > interval)
            {
                for (int x = 1; x < canvas.Width / interval; x++)
                {
                    for (int y = 1; y < canvas.Height / interval; y++)
                    {
                        textPath.Add(new ScreenPointF(x * interval, y * interval));
                    }
                }
            }
            else
            {
                textPath.Add(new ScreenPointF(canvas.Width / 2, canvas.Height / 2));
            }

            canvas.DrawText(watermarkText, font, brush, textPath, DrawingLevel.LevelOne);
        }
    }
}