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;
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!
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.