How to communicate between two extensions

I was asked by one developer how he might share data between two (DotSpatial) extensions and “light up” when the second extension was available.

This quick example shows exporting a method and importing it elsewhere by using a contract name. As long as the contract names and the signatures match, the export will be made available to the matching import by MEF.

public class MefImportPlugin : Extension
{
    public override void Activate()
    {
        App.HeaderControl.Add(new SimpleActionItem("Retrieve Message", ButtonClick));
        base.Activate();
    }

    public override void Deactivate()
    {
        App.HeaderControl.RemoveAll();
        base.Deactivate();
    }

    public void ButtonClick(object sender, EventArgs e)
    {
        var message = GetLayerCountMessage();
        System.Diagnostics.Trace.WriteLine(message);
    }

    [System.ComponentModel.Composition.Import("LayerCount.Message")]
    public Func<string> GetLayerCountMessage
    {
        get;
        set;
    }
}

 

public class MefExportPlugin : Extension
 {
     private string _Message;
     public override void Activate()
     {
         App.HeaderControl.Add(new SimpleActionItem("Show Layer Count", ButtonClick));
         base.Activate();
     }

     public override void Deactivate()
     {
         App.HeaderControl.RemoveAll();
         base.Deactivate();
     }

     public void ButtonClick(object sender, EventArgs e)
     {
         _Message = String.Format("Number of Layers: {0}", App.Map.Layers.Count);
         App.ProgressHandler.Progress(null, 0, _Message);
     }

     [System.ComponentModel.Composition.Export("LayerCount.Message")]
     public string Message()
     {
         return _Message;
     }
 }

These are standard Managed Extension Framework capabilities, so you can find more documentation by searching for MEF.

Using MEF Contracts to Coordinate Communication Between Extensions

Introduction

DotSpatial is an open-source project that contains controls which can be used to manipulate and display geographic information. This article explains how to allow extensions to communicate with one another. You will want to follow the introductory article, as we add additional functionality to the extension created in that article.

In many cases one class library will reference another in order to access functionality provided by the latter. Sometimes, however, these relationships are established at runtime to provide the user with more flexibility to mix and match components. In other cases, an extension is written so that it is itself extensible and will provide some service to other extensions which follow a proscribed convention, or it will “light-up” when other extensions expose the appropriate data or methods.

There are several approaches in DotSpatial for communicating between extensions. Named contracts require more documentation or inside knowledge and provide fewer guarantees about compatibility. Interface contracts are self-documenting but introduce more complexity, as the interface needs to be referenced. This article describes how to use Managed Extensibility Framework (MEF) named contracts. This article will not give you a complete understanding of MEF, but provides a simple example.

Getting Started with Exports

Open the project you created in the previous exercise. To keep things simple, we will expose the message that is being displayed in the status bar when the user clicks the Show Layer Count button. Widen the scope of the message variable, making it a field.

private string _Message;
public void ButtonClick(object sender, EventArgs e)
{
    _Message = String.Format("Number of Layers: {0}", App.Map.Layers.Count);
    App.ProgressHandler.Progress(null, 0, _Message);
}

Then, wrap the field in a method. The name of the method is not important, because we are using a named contract. If we were using an interface we would be able to use a property, instead, and expose it as part of the interface.

public string Message()
{
    return _Message;
}

The approach we will take for the string Message will work for any built-in .NET type. A similar approach will also work for collections. If you want to pass custom types between extensions, you will need to declare those in a separate assembly  and reference it in each extension project.

MEF uses the concept of Exports and Imports, where data, classes, or methods are exported by one class and imported by one or more classes. We mark our Message method for export by adding the Export attribute and specifying a contract name that will be used by all importers (“LayerCount.Message”).

[System.ComponentModel.Composition.Export("LayerCount.Message")]
public string Message()
{
    return _Message;
}

Getting Started with Imports

We will create another extension in this same project. Add a new class (Project, Add class…) named MefImport that derives from Extension. On Activate() a new SimpleActionItem should be added with the caption “Retrieve Message”. In the corresponding event handler, we retrieve the message and write it to the trace listeners. You’ll need to add the same using statements the plugin template includes.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using DotSpatial.Controls;
using DotSpatial.Controls.Header;

public class MefImport : Extension
{
    public override void Activate()
    {
        App.HeaderControl.Add(new SimpleActionItem("Retrieve Message", ButtonClick));
        base.Activate();
    }

    public override void Deactivate()
    {
        App.HeaderControl.RemoveAll();
        base.Deactivate();
    }

    public void ButtonClick(object sender, EventArgs e)
    {
        var message = GetLayerCountMessage();
        System.Diagnostics.Trace.WriteLine(message);
    }
}

We declare a property that is essentially a pointer to a function that returns a string. In C# the Func<> class represents a “pointer to a function” so our property looks like this:

public Func<string> GetLayerCountMessage
{
    get;
    set;
}

 

Lastly, we need MEF to import a contract by the name of “LayerCount.Message” so we add the import attribute.

[System.ComponentModel.Composition.Import("LayerCount.Message")]
public Func<string> GetLayerCountMessage
{
    get;
    set;
}

 

You can build and run the application. Click Show Layer Count, then Retrieve Message. Drag and drop a layer on the map, and then click Show Layer Count and Retrieve Message, again.

image

Check the output window (Debug, Windows, Output) to see that the message was properly retrieved when GetLayerCountMessage() was called.

What if the exporting extension wasn’t available?

If we expected the exporting extension to only occasionally be available, we can use a named parameter of the Import attribute to indicate that instead of throwing an exception, we would like MEF to leave our GetLayerCountMessage property with its default value (null). Of course, we have to check for null before using the property, in that case.

public void ButtonClick(object sender, EventArgs e)
{
    if (GetLayerCountMessage != null)
    {
        var message = GetLayerCountMessage();
        System.Diagnostics.Trace.WriteLine(message);
    }
}

[System.ComponentModel.Composition.Import("LayerCount.Message", AllowDefault = true)]
public Func<string> GetLayerCountMessage
{
    get;
    set;
}

 

Now we can prevent the Layer Count extension from loading by commenting it out. When running the application, we will still find the Retrieve Message menu item, and clicking it won’t cause any problem.

Points of Interest

if multiple extensions will be exporting a given contract, the importer should use the ImportMany attribute and import an IEnumerable<> of the original, expected type.

Trying to expose Message as a property under a named contract won’t work because MEF will perform the import (and export) only once, by default. We would see the original null message, and nothing else.

Fields can also be exported and imported, so as a matter of style, I used properties.

How to Sort Layers Alphabetically

Introduction

DotSpatial is an open-source project that contains controls which can be used to manipulate and display geographic information. This article explains how to create a DotSpatial extension by using the online template. The extension we are creating will allow the user to sort the layers so that they appear in the Legend alphabetically.

Getting Started

If you are not familiar with creating a simple DotSpatial-based extension, please consider the introductory article. For practical purposes, we assume you are coming to this article after having completed the previous one. This article is based on DotSpatial 1.2.

Creating a New Project

Create a new project using the DotSpatial Plugin Template. You may delete the Readme.txt and modify the name of the MyPlugin1 class (to reflect the functionality provided by the extension). I named mine SortLayersPlugin. Change the caption of the SimpleActionItem from “My Button Caption” to “Sort Layers”.

Replace the ButtonClick event handler with the following code, which is explained inline.

public void ButtonClick(object sender, EventArgs e)
{
    // Get a list of layers sorted alphabetically by LegendText.
    var newLayers = App.Map.Layers.OrderByDescending(l => l.LegendText).ToList();

    // Suspending events speeds our changes up and prevents redrawing multiple times.
    // The events will be called only once, when we call ResumeEvents().
    App.Map.Layers.SuspendEvents();

    // By default, it appears some part of the layer is disposed when it is removed from the collection.
    // If we were to use App.Map.Layers.Clear(), we would stlil need to LockDispose on each layer.
    while (App.Map.Layers.Any())
    {
        var layer = App.Map.Layers[0];
        layer.LockDispose();
        App.Map.Layers.RemoveAt(0);
    }

    // As we add each layer back in, in the correct order, we UnlockDispose so that the layer can be disposed
    // at the appropriate time.
    foreach (IMapLayer newLayer in newLayers)
    {
        App.Map.Layers.Add(newLayer);
        newLayer.UnlockDispose();
    }
    App.Map.Layers.ResumeEvents();
}

Conclusion.

Build and run the application. You can add a few layers and then use the menu item to sort them.

image

Points of Interest

You can add another button to sort the layers in reverse alphabetical order by replacing App.Map.Layers.OrderByDescending(…)… with App.Map.Layers.OrderBy(…)….

How to Add Support for Loading DotSpatial.Plugins.Ribbon

Introduction

DotSpatial is an open-source project that contains controls which can be used to manipulate and display geographic information. DotSpatial can be extended with a number of extensions. Some of these extensions require the application developer provide additional support before they will load. We look at adding support for those that require a Shell export, which includes the extensions providing ribbon and docking features.

Getting Started

You will need to implement basic extensions loading support as described: How to Support Loading Extensions in DotSpatial. For practical purposes, we assume you are coming to this article after having completed the previous one.

Extensions may depend on other extensions being available. They may also “light up” synergistically when other extensions are available. This behavior is made possible through the Managed Extension Framework (MEF) via a set of Import and Export attributes. Since MEF is available on MSDN, instead of explaining how it works, I’ll cover how the DockManager, Ribbon, and ToolManager plugins use it.

Windows Forms

In order for an extension like the Ribbon to register itself properly with the main form of your application, it must be told what form that is. Your application could include a number of forms, so one of them must be specified as the Shell.

This functionality relies on MEF, and not on DotSpatial, because it is not directly related to mapping. So, instead of creating an interface in DotSpatial, we provide a convention to be followed.

Specifying a Shell

The application should Export a field or property with the contact name of “Shell” and type of ContainerControl. In MapWindow 6, this is added as a field to the MainForm class (as shown).

[Export("Shell", typeof(ContainerControl))]
private static ContainerControl Shell;

At any point before LoadExtensions() is called, this field should be set to the form that is considered the Shell. We do this in the MainForm constructor in MapWindow 6.

Shell = this;
appManager.LoadExtensions();

The Ribbon extension imports Shell when it is being loaded and adds a ribbon control to the associated form’s control collection. You can similarly export other types from one extension to another, and create a more loosely coupled set of classes.

Points of Interest

You will want to obtain a copy of the Ribbon or DockManager so that you can test your implementation. These can be pulled as packages from the DotSpatial feed (gallery).

Composing an Application by Combining Extensions

Introduction

DotSpatial is an open-source project that contains controls which can be used to manipulate and display geographic information. MapWindow 6 is a thin wrapper around DotSpatial. This article explains how to create an application like MapWindow 6 or HydroDesktop, by combining extensions to get the desired set of functionality. Some extensions are included by default with DotSpatial builds while others can be found in the Extension Manager.

Obtaining Extensions

Presently, we don’t make any distinction between the terms add-on, plugin, or extension. In general, these refer to a class that provides some functionality designed to complement DotSpatial and inherits from IExtension (or implements Extension).

There are other types of extensions that provide a more specific type of functionality such as DataProviders, HeaderControls, and ProgressHandlers. These inherit from specific interfaces such as IDataProvider, IHeaderControl, IProgressHandler and so on. They are less general purpose in nature.

Extensions can be obtain in a number of ways:

Codeplex Downloads – the Extended release package includes additional extensions.

DotSpatial Package Feed – updated extensions are built and pushed to the feed automatically. The feed may also include user-contributed (closed source) packages and is used by the Extension Manager.

The Extension Manager – the extension manager itself is an extension. It allows the user to download additional extensions.

You – you can create your own DotSpatial extensions as well. Then you can compose various applications by including the appropriate extensions that result in the desired set of features.

Installing Extensions

There are three different places an extension may be placed. Each yields slightly different functionality. A programmer may also add additional directories to the appManager.Directories property so that additional directories are searched. When installing an extension, you’ll need the extension assembly (dll) as well as any of its dependencies. The built in assembly resolver allows you to create an extension that depends on another.

Plugins folder – when a developer builds the DotSpatial project, extensions are placed in the Plugins folder inside of the application folder. These extensions (assuming they inherit from IExtension) may be activated or deactivated by the end-user. If you are deploying your application with a specific set of extensions that you wish to behave in this way, this is the right place for them.

Application Extensions folder – extensions placed here will be activated when the application launches; the user will not be able to deactivate them. This allows the developer to create a set of extensions that make up the branding of the application. From the user perspective, these are part of “the application”, and the developer can use this flexibility to divide up application logic into multiple extensions. An end user wouldn’t be able to tell whether the feature was built into DotSpatial, or was an extension.

Extensions folder – this folder is placed in the user profile folder inside of a folder based on the application assembly name. You can open this folder by clicking Show Extensions Folder in the Extension Manager.

image

The path would look something like %appdata%\DemoMap.exe\Extensions.

When users install or update packages from the online section of the Extension Manager, they are placed here.

Note: If an extension was originally in the Plugins folder, and the user chooses to update it, the new version will be placed in the Extensions folder and the existing version will be removed (after a restart).

Loading Extensions

When an extension is loaded, we guarantee that it will be able to access an AppManager, IProgressHandler, and IHeaderControl. This allows the developer to avoid checking whether these properties are null before each use. This means, however, that extensions fulfilling these requirements must be available. In DemoMap.exe implementations are included in the project itself as classes. In MapWindow 6, example implementations are included as extensions. You can use whichever suits you.

You may want to review a few related articles:

How to Support Loading Extensions (Loading GDAL)

How to Load DotSpatial Extensions Into My Toolbar

Case Study

The MapWindow 6 application is strictly a set of extensions, the DotSpatial library, a small amount of code to allow you to brand it with your own splash screen image and name, and a little code to deal with command line parameters.

The HydroDesktop application is a copy of MapWindow 6 with a different set of extensions and custom branding. Eventually, HydroDesktop can become more of an “extension pack” so that it can be downloaded into any DotSpatial compatible application.

Points of Interest

See Also: Using the Extension Manager

Extensions can communicate with one another using the Managed Extensibility Framework (MEF).

Setting AppManager.UseBaseDirectoryForExtensionsDirectory can alter the location where extensions are installed by the Extension Manager.

Using PointSymbolizer on a DrawingLayer

Introduction

DotSpatial is an open-source project that contains controls which can be used to manipulate and display geographic information. This article explains how to create a DotSpatial extension by using the online template. If you are not familiar with creating a simple DotSpatial-based extension, please consider the introductory article. The extension we are creating will show a star georeferenced to each point where the user right clicks.

Getting Started

Create a new project using the DotSpatial Plugin Template. You may delete the Readme.txt and modify the name of the MyPlugin1 class (to reflect the functionality provided by the extension). I named mine PointSymbolizerPlugin. It is also reasonable to rename the template SimpleActionItem from “My Button Caption” to “Create Stars On Right-Click”

Creating a MapPointLayer

We’ll use the ButtonClick event to start tracking the mouse so that we can capture coordinates and draw points on the map surface. The MapPointLayer will hold the collection of points and a PointSymbolizer will be responsible for drawing them.

public void ButtonClick(object sender, EventArgs e)
{
    map = App.Map as Map;

    // Enable left click panning and mouse wheel zooming
    map.FunctionMode = FunctionMode.Pan;

    // Handle mouse up event on the map
    map.MouseUp += map_MouseUp;

    // The FeatureSet starts with no data; be sure to set it to the point featuretype
    _markers = new FeatureSet(FeatureType.Point);

    // The MapPointLayer controls the drawing of the marker features
    _markerLayer = new MapPointLayer(_markers);

    // The Symbolizer controls what the points look like
    _markerLayer.Symbolizer = new PointSymbolizer(Color.Blue, Symbology.PointShape.Star, 15);

    // A drawing layer draws on top of data layers, but is still georeferenced.
    map.MapFrame.DrawingLayers.Add(_markerLayer);
}

You will notice that we use a few class level variables (fields). Add them to class by placing this code inside the class and outside of any methods, preferably at the top of the class.

    private Map map;
    private FeatureSet _markers;
    private MapPointLayer _markerLayer;

To keep the code succinct, add these statements beneath the other using statements, which are near the top of the file.

    using System.Drawing;
    using System.Windows.Forms;
    using DotSpatial.Data;
    using DotSpatial.Symbology;
    using DotSpatial.Topology;

References should be added (Project, Add Reference) to System.Windows.Forms and System.Drawing. These assemblies contain code related to MouseUp event and to the Color Blue, respectively.

Adding Features to a FeatureSet

We will use the Mouse up event to intercept right clicks and add features (points) to the feature set and layer we created earlier. These points will be symbolized (drawn) by the PointSymbolizer when the MapFrame is invalidated. This happens when the window is resized or when we programatically invoke the Invalidate() method.

void map_MouseUp(object sender, MouseEventArgs e)
{
    // Intercept only the right click for adding markers
    if (e.Button != MouseButtons.Right) return;

    // Get the geographic location that was clicked
    Coordinate c = map.PixelToProj(e.Location);

    // Add the new coordinate as a "point" to the point featureset
    _markers.AddFeature(new DotSpatial.Topology.Point(c));

    // Drawing will take place from a bitmap buffer, so if data is updated,
    // we need to tell the map to refresh the buffer 
    map.MapFrame.Invalidate();
}

It is important that we convert the mouse location to the proper map coordinate based on the current map projection, by calling PixelToProj() with the mouse coordinates.

Cleaning Up on Deactivate()

Though the template doesn’t include an extension manager that would let us deactivate or activate the extension, let’s consider how we should deal with the case where the extension was uninstalled while running or where it was deactivated by the user.

The Deactivate method already has a call to HeaderControl.RemoveAll(). This means the menu item will be removed. We need to also remove our layer and redraw the map. Add this code to the Deactivate method under the RemoveAll() method call.

if (map != null && map.MapFrame.DrawingLayers.Contains(_markerLayer))
{
    // Remove our drawing layer from the map.
    map.MapFrame.DrawingLayers.Remove(_markerLayer);

    // Request a redraw
    map.MapFrame.Invalidate();
}

Conclusion

Build and run your project. Add a layer (e.g., bgd file) to the map for reference by dragging and dropping the file onto the map control. Click your extension menu item and add a few points to the map. When you zoom in or out using the mouse wheel, any stars will redrawn so that their size remains constant.

image

Points Of Interest

You can drop the DotSpatial.Plugins.ExtensionManager.dll and NuGet.Core in your output Plugins folder if you would like test deactivating your extension. I noticed that the current DemoMap implementation will not add the menu item back in if you reactivate the extension. You’ll have to run the project again.

Create an Extension to Analyze Vector and Raster Data

Introduction

DotSpatial is an open-source project that contains controls which can be used to manipulate and display geographic information. This article explains how to create a DotSpatial extension by using the online template. The extension we are creating will allow the user to draw a line and create a line layer and show elevation of a path on a Digital Elevation Model (DEM).

Getting Started

If you are not familiar with creating a simple DotSpatial-based extension, please consider the introductory article. For practical purposes, we assume you are coming to this article after having completed the previous one.

Creating a New Project

Create a new project using the DotSpatial Plugin Template. You may delete the Readme.txt and modify the name of the MyPlugin1 class (to reflect the functionality provided by the extension). I named mine PlotPathElevationPlugin.

Creating the Chart

We’ll be using a standard Windows Forms chart to plot the elevation. Create a new Form (Project, Add Windows Form…) named ChartForm.

Add a Chart control to your form. This can be done by double clicking the Chart item in the Data tab of the Toolbox (View, Toolbox). In the Properties Window (View, Properties Window) set Dock to Fill.

We only need to add one method to the form class to create a visual plot of the data that will be passed in (View, Code).

public void Plot(double[] data)
{
    chart1.Series.Clear();
    var series = chart1.Series.Add("Elevation (meters)");
    series.ChartType = System.Windows.Forms.DataVisualization.Charting.SeriesChartType.Line;
    series.Points.DataBindY(data);
}

When we call the Plot method with an array of elevation data, it will make a simple line chart out of it.

Getting Raster Data

A DEM raster is essentially a rectangular grid with an elevation value for each cell. The elevation values are often represented visually by a color gradient. DotSpatial allows us to retrieve these elevation values by reaching into the DataSet for a particular row and column.

We’ll add the rest of our code to the PlotPathElevationPlugin class. Add these statements beneath the other using statements, which are near the top of the file.

    using System.Data;
    using System.Drawing;
    using System.Windows.Forms;
    using DotSpatial.Data;
    using DotSpatial.Symbology;
    using DotSpatial.Topology;

Create a method that returns the elevation for a given coordinate.

private static double GetElevation(IMapRasterLayer raster, Coordinate coordinate)
{
    RcIndex rowColumn = raster.DataSet.Bounds.ProjToCell(coordinate);
    double elevation = raster.DataSet.Value[rowColumn.Row, rowColumn.Column];
    return elevation;
}

We also create a method to get the linear distance between two points.

private static double GetDistance(double x1, double y1, double x2, double y2)
{
    return Math.Sqrt(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)));
}

Coordinates

Much of the work of this extension is in collecting a list of coordinates provided by the user (via mouse clicks on the map) and expanding those coordinates into a larger set of points whose elevation will be plotted. The next two methods demonstrate how we can get a list of coordinates from a line layer. Later, we’ll cover how to create this line feature using LineString.

private List<Coordinate> GetCoordinatesFromLine(IMapLineLayer lineLayer)
{
    IFeatureSet featureSet = lineLayer.DataSet;

    // The coordinates should be the first feature of the feature set.
    IList<Coordinate> lineCoordinates = featureSet.Features[0].Coordinates;

    // Though the original line may only have a few points, we split
    // each line segment into many points
    List<Coordinate> pathCoordinates = new List<Coordinate>();

    for (int i = 0; i < lineCoordinates.Count - 1; i++)
    {
        Coordinate startCoord = lineCoordinates[i];
        Coordinate endCoord = lineCoordinates[i + 1];
        List<Coordinate> segmentCoordinates = SplitSegment(startCoord.X, startCoord.Y, endCoord.X, endCoord.Y);

        //add list of points from this line segment to the complete list
        pathCoordinates.AddRange(segmentCoordinates);
    }
    return pathCoordinates;
}

private static List<Coordinate> SplitSegment(double startX, double startY, double endX, double endY)
{
    const int MinimumDistanceBetweenPoints = 15;

    double points = Math.Floor(GetDistance(startX, startY, endX, endY) / MinimumDistanceBetweenPoints);
    int PointsPerSegment = (int)Math.Max(points, 1);

    double curX = startX;
    double curY = startY;
    double constXdif = ((endX - startX) / PointsPerSegment);
    double constYdif = ((endY - startY) / PointsPerSegment);

    List<Coordinate> pathPointList = new List<Coordinate>(PointsPerSegment);
    for (int i = 0; i <= PointsPerSegment; i++)
    {
        if (i == 0)
        {
            curX = startX;
            curY = startY;
        }
        else
        {
            curX = curX + constXdif;
            curY = curY + constYdif;
        }
        Coordinate coordinate = new Coordinate(curX, curY);
        pathPointList.Add(coordinate);
    }
    return pathPointList;
}

Class Level Variables

We will need several class level variables: an IFeature will represent our line while the user is drawing it, a MapLineLayer reference will be stored so that we can remove the user drawn path if they wish to start over, and a Map, which will point to the Application Map, though it will be cast to the Windows Forms type of map.

Add these fields to your class as shown.

    private IFeature _LineFeature;
    private MapLineLayer _PathLineLayer;
    Map map;

image

Converting Coordinates to Elevation

With a little code to glue things together, we’ll have the ability to take a line (path) and a raster (DEM) and plot the elevation of the path over the DEM. In the following sections we will work on allowing the user to create the line.

private void ShowElevation()
{
    if (!map.GetRasterLayers().Any())
    {
        MessageBox.Show("Please add a DEM raster layer to the map.");
        return;
    }

    if (!map.GetLineLayers().Any())
    {
        MessageBox.Show("Please create a path by left clicking to add points and right-clicking to complete the path.");
        return;
    }

    try
    {
        IMapRasterLayer rasterLayer = map.GetRasterLayers().First();
        IMapLineLayer pathLayer = map.GetLineLayers().First();
        var coords = GetCoordinatesFromLine(pathLayer);

        double[] elevation = new double[coords.Count];
        for (int i = 0; i < coords.Count; i++)
        {
            elevation[i] = GetElevation(rasterLayer, coords[i]);
        }

        ChartForm chart = new ChartForm();
        chart.Plot(elevation);
        chart.Show();
    }
    catch (Exception ex)
    {
        MessageBox.Show("Error calculating elevation. The whole path should be inside the DEM area. " + ex.Message);
    }
}

Using the SimpleActionItem (menu item)

Change the name of the SimpleActionItem from “My Button Caption” to “Plot Elevation of Path”.  We’ll use the ButtonClick event to start tracking the mouse so that we can capture coordinates and draw a line on the map surface. We’ll assume that the user probably already loaded a DEM, and we’ll prompt them if they try to draw a path without a DEM under it.

Replace the ButtonClick event handler.

public void ButtonClick(object sender, EventArgs e)
{
    // We're expecting this extension to only be run in a Windows Forms application.
    // We'll depend on a few Windows Forms (Map) features like MouseDown, so we cast
    // the App.Map as a Map and store a reference to it.
    map = App.Map as Map;

    // remove any existing path if needed.
    if (_PathLineLayer != null)
        map.Layers.Remove(_PathLineLayer);

    _PathLineLayer = null;
    _LineFeature = null;

    // Let the user know we are ready for them to set points by changing the cursor.
    map.Cursor = Cursors.Cross;
    map.MouseDown += map_MouseDown;
}

Creating a Vector Layer

You’ll notice the previous code block references the map_MouseDown method, which hasn’t yet been added. You can find the find the remaining code below. We use the MouseDown event to start of continue the process of adding points to the line as well as showing a progress method to provide the user with a hint on how to continue.

The current extension template DemoMap overwrites the status frequently with the current mouse coordinate, so you may see this message only briefly when using the application.

When the user signals they are done drawing the path, we show them the elevation plot.

We add several columns to the attribute table for our feature, and update these as the user creates the path.

private void map_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        // Encourage the user to select a raster, if they haven't done so.
        if (!map.GetRasterLayers().Any())
        {
            map.AddRasterLayer();
            map.ZoomToMaxExtent();
            return;
        }

        StartOrContinueDrawingPath(e.Location);
        App.ProgressHandler.Progress(null, 0, "Point registered. Click again to add line segment. Right-click to finish.");
    }
    else if (e.Button == MouseButtons.Right)
    {
        EndDrawingPath();
        ShowElevation();
        App.ProgressHandler.Progress(null, 0, "Ready.");
    }
}

private IFeature AddLineFeatureSetToMap()
{
    FeatureSet lineFeatureSet = new FeatureSet(FeatureType.Line);
    lineFeatureSet.Projection = map.Projection;

    // Initialize the featureSet attribute table by creating columns
    DataColumn column = new DataColumn("ID", typeof(short));
    lineFeatureSet.DataTable.Columns.Add(column);
    DataColumn column2 = new DataColumn("Number of Points", typeof(int));
    lineFeatureSet.DataTable.Columns.Add(column2);
    DataColumn column3 = new DataColumn("Description");
    lineFeatureSet.DataTable.Columns.Add(column3);

    // Add the featureSet as map layer
    _PathLineLayer = (MapLineLayer)map.Layers.Add(lineFeatureSet);
    _PathLineLayer.Symbolizer = new LineSymbolizer(Color.Blue, 2);
    _PathLineLayer.LegendText = "Path Layer";

    var newList = new List<Coordinate>();
    LineString lineGeometry = new LineString(newList);

    // AddFeature creates the point and a row in the DataTable
    return lineFeatureSet.AddFeature(lineGeometry);
}

private void StartOrContinueDrawingPath(System.Drawing.Point mouseLocation)
{
    Coordinate coord = map.PixelToProj(mouseLocation);

    if (_LineFeature == null)
    {
        // This is the first time we see a left click; create empty line feature.
        _LineFeature = AddLineFeatureSetToMap();

        // Add first coordinate to the line feature.
        _LineFeature.Coordinates.Add(coord);

        // Set the line feature attribute. This line may have multiple points,
        // but there is only one row in the attribute table for the entire feature (line).
        _LineFeature.DataRow["ID"] = 0;
        _LineFeature.DataRow["Description"] = "Path (line)";
    }
    else
    {
        // Second or later click - add points to the existing feature
        _LineFeature.BasicGeometry.Coordinates.Add(coord);
        _LineFeature.ParentFeatureSet.InitializeVertices();

        // Draw the line.
        map.ResetBuffer();

        // Update the attribute table.
        _LineFeature.DataRow["Number of Points"] = _LineFeature.BasicGeometry.Coordinates.Count;
    }
}

private void EndDrawingPath()
{
    // The path is complete.
    map.ResetBuffer();
    map.Cursor = Cursors.Arrow;
    map.MouseDown -= map_MouseDown;
    _LineFeature = null;
}

Conclusion

Build and run you application, and click away!

image

Points of Interest

DotSpatial only supports .bgd rasters by default. You’ll need the GDAL extension, for example if you want to open .tif files.

We kept things simple, so the X axis on your plot will only roughly represent relative distance.